From 7105fc1a4a1fc25654afce18fd0daffd5f0e70e1 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 9 Jan 2024 10:47:09 +0100 Subject: [PATCH 1/8] chore: Adapt dlc channels api to return dlc channel instead of sub channels --- coordinator/src/admin.rs | 44 +++++----- .../ln-dlc-node/src/ln/dlc_channel_details.rs | 85 +++++++------------ 2 files changed, 56 insertions(+), 73 deletions(-) diff --git a/coordinator/src/admin.rs b/coordinator/src/admin.rs index 73258d622..b403d56b7 100644 --- a/coordinator/src/admin.rs +++ b/coordinator/src/admin.rs @@ -15,8 +15,8 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; use commons::CollaborativeRevertCoordinatorExpertRequest; use commons::CollaborativeRevertCoordinatorRequest; +use dlc_manager::channel::signed_channel::SignedChannel; use dlc_manager::contract::Contract; -use dlc_manager::subchannel::SubChannel; use lightning_invoice::Bolt11Invoice; use ln_dlc_node::node::NodeInfo; use serde::de; @@ -246,10 +246,17 @@ pub struct DlcChannelDetails { pub user_registration_timestamp: Option, } -impl From<(SubChannel, Option, String, Option)> for DlcChannelDetails { +impl + From<( + SignedChannel, + Option, + String, + Option, + )> for DlcChannelDetails +{ fn from( (channel_details, contract, user_email, user_registration_timestamp): ( - SubChannel, + SignedChannel, Option, String, Option, @@ -273,36 +280,31 @@ pub async fn list_dlc_channels( AppError::InternalServerError(format!("Failed to acquire db lock: {e:#}")) })?; - let dlc_channels = state.node.inner.list_sub_channels().map_err(|e| { + let dlc_channels = state.node.inner.list_dlc_channels().map_err(|e| { AppError::InternalServerError(format!("Failed to list DLC channels: {e:#}")) })?; let dlc_channels = dlc_channels .into_iter() - .map(|subchannel| { + .map(|dlc_channel| { let (email, registration_timestamp) = - match db::user::by_id(&mut conn, subchannel.counter_party.to_string()) { + match db::user::by_id(&mut conn, dlc_channel.counter_party.to_string()) { Ok(Some(user)) => (user.email, Some(user.timestamp)), _ => ("unknown".to_string(), None), }; - let dlc_channel_id = subchannel.get_dlc_channel_id(0); - - let contract = match dlc_channel_id { - Some(dlc_channel_id) => { - match state - .node - .inner - .get_contract_by_dlc_channel_id(&dlc_channel_id) - { - Ok(contract) => Some(contract), - Err(_) => None, - } - } - None => None, + let dlc_channel_id = dlc_channel.channel_id; + + let contract = match state + .node + .inner + .get_contract_by_dlc_channel_id(&dlc_channel_id) + { + Ok(contract) => Some(contract), + Err(_) => None, }; - DlcChannelDetails::from((subchannel, contract, email, registration_timestamp)) + DlcChannelDetails::from((dlc_channel, contract, email, registration_timestamp)) }) .collect::>(); diff --git a/crates/ln-dlc-node/src/ln/dlc_channel_details.rs b/crates/ln-dlc-node/src/ln/dlc_channel_details.rs index 771de0c6f..03a46de09 100644 --- a/crates/ln-dlc-node/src/ln/dlc_channel_details.rs +++ b/crates/ln-dlc-node/src/ln/dlc_channel_details.rs @@ -1,6 +1,6 @@ use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; -use dlc_manager::subchannel::SubChannel; +use dlc_manager::channel::signed_channel::SignedChannel; use dlc_manager::DlcChannelId; use lightning::ln::ChannelId; use serde::Serialize; @@ -8,71 +8,59 @@ use serde::Serializer; #[derive(Serialize, Debug)] pub struct DlcChannelDetails { - #[serde(serialize_with = "channel_id_as_hex")] - pub channel_id: ChannelId, #[serde(serialize_with = "optional_channel_id_as_hex")] pub dlc_channel_id: Option, #[serde(serialize_with = "pk_as_hex")] pub counter_party: PublicKey, pub update_idx: u64, - pub subchannel_state: SubChannelState, + pub subchannel_state: SignedChannelState, pub fee_rate_per_vb: u64, - pub fund_value_satoshis: u64, - /// Whether the local party is the one who offered the sub channel. - pub is_offer: bool, } #[derive(Serialize, Debug)] -pub enum SubChannelState { - Offered, - Accepted, - Finalized, - Signed, +pub enum SignedChannelState { + Established, + SettledOffered, + SettledReceived, + SettledAccepted, + SettledConfirmed, + Settled, + RenewOffered, + RenewAccepted, + RenewConfirmed, + RenewFinalized, Closing, - OnChainClosed, - CounterOnChainClosed, - CloseOffered, - CloseAccepted, - CloseConfirmed, - OffChainClosed, - ClosedPunished, - Confirmed, - Rejected, + CollaborativeCloseOffered, } -impl From for DlcChannelDetails { - fn from(sc: SubChannel) -> Self { +impl From for DlcChannelDetails { + fn from(sc: SignedChannel) -> Self { DlcChannelDetails { - channel_id: sc.channel_id, - dlc_channel_id: sc.get_dlc_channel_id(0), + dlc_channel_id: Some(sc.channel_id), counter_party: sc.counter_party, update_idx: sc.update_idx, - subchannel_state: SubChannelState::from(sc.state), + subchannel_state: SignedChannelState::from(sc.state), fee_rate_per_vb: sc.fee_rate_per_vb, - fund_value_satoshis: sc.fund_value_satoshis, - is_offer: sc.is_offer, } } } -impl From for SubChannelState { - fn from(value: dlc_manager::subchannel::SubChannelState) -> Self { - use dlc_manager::subchannel::SubChannelState::*; +impl From for SignedChannelState { + fn from(value: dlc_manager::channel::signed_channel::SignedChannelState) -> Self { + use dlc_manager::channel::signed_channel::SignedChannelState::*; match value { - Offered(_) => SubChannelState::Offered, - Accepted(_) => SubChannelState::Accepted, - Finalized(_) => SubChannelState::Finalized, - Signed(_) => SubChannelState::Signed, - Closing(_) => SubChannelState::Closing, - OnChainClosed => SubChannelState::OnChainClosed, - CounterOnChainClosed => SubChannelState::CounterOnChainClosed, - CloseOffered(_) => SubChannelState::CloseOffered, - CloseAccepted(_) => SubChannelState::CloseAccepted, - CloseConfirmed(_) => SubChannelState::CloseConfirmed, - OffChainClosed => SubChannelState::OffChainClosed, - ClosedPunished(_) => SubChannelState::ClosedPunished, - Confirmed(_) => SubChannelState::Confirmed, - Rejected => SubChannelState::Rejected, + Established { .. } => SignedChannelState::Established, + SettledOffered { .. } => SignedChannelState::SettledOffered, + SettledReceived { .. } => SignedChannelState::SettledReceived, + SettledAccepted { .. } => SignedChannelState::SettledAccepted, + SettledConfirmed { .. } => SignedChannelState::SettledConfirmed, + Settled { .. } => SignedChannelState::Settled, + RenewOffered { .. } => SignedChannelState::RenewOffered, + RenewAccepted { .. } => SignedChannelState::RenewAccepted, + RenewConfirmed { .. } => SignedChannelState::RenewConfirmed, + RenewFinalized { .. } => SignedChannelState::RenewFinalized, + Closing { .. } => SignedChannelState::Closing, + CollaborativeCloseOffered { .. } => SignedChannelState::CollaborativeCloseOffered, } } } @@ -87,13 +75,6 @@ where } } -fn channel_id_as_hex(channel_id: &ChannelId, s: S) -> Result -where - S: Serializer, -{ - s.serialize_str(&hex::encode(channel_id.0)) -} - fn pk_as_hex(pk: &PublicKey, s: S) -> Result where S: Serializer, From 6f214c791aca1423746e25c61fe40e00d67aab62 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 9 Jan 2024 10:48:04 +0100 Subject: [PATCH 2/8] chore: Fix compile errors in test-e2e crate --- crates/tests-e2e/src/fund.rs | 2 +- crates/tests-e2e/src/setup.rs | 4 ++-- crates/tests-e2e/tests/basic.rs | 2 +- .../tests-e2e/tests/receive_payment_when_open_position.rs | 6 +++--- crates/tests-e2e/tests/restore_from_backup.rs | 4 ++-- crates/tests-e2e/tests/send_payment_when_open_position.rs | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/tests-e2e/src/fund.rs b/crates/tests-e2e/src/fund.rs index 5b6b2c547..d4eab2c82 100644 --- a/crates/tests-e2e/src/fund.rs +++ b/crates/tests-e2e/src/fund.rs @@ -51,7 +51,7 @@ pub async fn fund_app_with_faucet( tracing::info!(%fund_amount, %fee_sats, "Successfully funded app with faucet"); assert_eq!( - app.rx.wallet_info().unwrap().balances.lightning, + app.rx.wallet_info().unwrap().balances.off_chain, fund_amount - fee_sats ); diff --git a/crates/tests-e2e/src/setup.rs b/crates/tests-e2e/src/setup.rs index a239d6b67..2f5d5c22f 100644 --- a/crates/tests-e2e/src/setup.rs +++ b/crates/tests-e2e/src/setup.rs @@ -40,7 +40,7 @@ impl TestSetup { let app = run_app(None).await; assert_eq!( - app.rx.wallet_info().unwrap().balances.lightning, + app.rx.wallet_info().unwrap().balances.off_chain, 0, "App should start with empty wallet" ); @@ -50,7 +50,7 @@ impl TestSetup { .await .unwrap(); - let ln_balance = app.rx.wallet_info().unwrap().balances.lightning; + let ln_balance = app.rx.wallet_info().unwrap().balances.off_chain; tracing::info!(%fund_amount, %ln_balance, "Successfully funded app with faucet"); assert!(ln_balance > 0, "App wallet should be funded"); diff --git a/crates/tests-e2e/tests/basic.rs b/crates/tests-e2e/tests/basic.rs index 68b962567..f896b08de 100644 --- a/crates/tests-e2e/tests/basic.rs +++ b/crates/tests-e2e/tests/basic.rs @@ -29,7 +29,7 @@ async fn app_can_be_funded_with_lnd_faucet() -> Result<()> { let app = run_app(None).await; // Unfunded wallet should be empty - assert_eq!(app.rx.wallet_info().unwrap().balances.lightning, 0); + assert_eq!(app.rx.wallet_info().unwrap().balances.off_chain, 0); // open channel fees should be 11_000 sats (1%) let fund_amount = 1_100_000; diff --git a/crates/tests-e2e/tests/receive_payment_when_open_position.rs b/crates/tests-e2e/tests/receive_payment_when_open_position.rs index b32f33ad2..1855f8430 100644 --- a/crates/tests-e2e/tests/receive_payment_when_open_position.rs +++ b/crates/tests-e2e/tests/receive_payment_when_open_position.rs @@ -9,7 +9,7 @@ async fn can_receive_payment_with_open_position() { let test = setup::TestSetup::new_with_open_position().await; let app = &test.app; - let ln_balance_before = app.rx.wallet_info().unwrap().balances.lightning; + let ln_balance_before = app.rx.wallet_info().unwrap().balances.off_chain; let invoice_amount = 10_000; tracing::info!("Creating an invoice"); @@ -21,8 +21,8 @@ async fn can_receive_payment_with_open_position() { tracing::info!("Coordinator pays the invoice of {invoice_amount} sats created in the app"); test.coordinator.pay_invoice(&invoice).await.unwrap(); - wait_until!(app.rx.wallet_info().unwrap().balances.lightning > ln_balance_before); - let ln_balance = app.rx.wallet_info().unwrap().balances.lightning; + wait_until!(app.rx.wallet_info().unwrap().balances.off_chain > ln_balance_before); + let ln_balance = app.rx.wallet_info().unwrap().balances.off_chain; tracing::info!(%ln_balance, %ln_balance_before, %invoice_amount, "Lightning balance increased"); assert_eq!(ln_balance, ln_balance_before + invoice_amount); diff --git a/crates/tests-e2e/tests/restore_from_backup.rs b/crates/tests-e2e/tests/restore_from_backup.rs index d38088d7a..65e9524ba 100644 --- a/crates/tests-e2e/tests/restore_from_backup.rs +++ b/crates/tests-e2e/tests/restore_from_backup.rs @@ -16,7 +16,7 @@ async fn app_can_be_restored_from_a_backup() { let seed_phrase = api::get_seed_phrase(); - let ln_balance = test.app.rx.wallet_info().unwrap().balances.lightning; + let ln_balance = test.app.rx.wallet_info().unwrap().balances.off_chain; // kill the app test.app.stop(); @@ -24,7 +24,7 @@ async fn app_can_be_restored_from_a_backup() { let app = run_app(Some(seed_phrase.0)).await; - assert_eq!(app.rx.wallet_info().unwrap().balances.lightning, ln_balance); + assert_eq!(app.rx.wallet_info().unwrap().balances.off_chain, ln_balance); let positions = spawn_blocking(|| api::get_positions().unwrap()) .await diff --git a/crates/tests-e2e/tests/send_payment_when_open_position.rs b/crates/tests-e2e/tests/send_payment_when_open_position.rs index ce6f5b167..94c4dc265 100644 --- a/crates/tests-e2e/tests/send_payment_when_open_position.rs +++ b/crates/tests-e2e/tests/send_payment_when_open_position.rs @@ -10,7 +10,7 @@ async fn can_send_payment_with_open_position() { let test = setup::TestSetup::new_with_open_position().await; let app = &test.app; - let ln_balance_before = app.rx.wallet_info().unwrap().balances.lightning; + let ln_balance_before = app.rx.wallet_info().unwrap().balances.off_chain; let invoice_amount = 10_000; tracing::info!("Create an invoice in the coordinator"); @@ -31,6 +31,6 @@ async fn can_send_payment_with_open_position() { .await .unwrap(); - wait_until!(app.rx.wallet_info().unwrap().balances.lightning < ln_balance_before); - assert!(app.rx.wallet_info().unwrap().balances.lightning <= ln_balance_before - invoice_amount); + wait_until!(app.rx.wallet_info().unwrap().balances.off_chain < ln_balance_before); + assert!(app.rx.wallet_info().unwrap().balances.off_chain <= ln_balance_before - invoice_amount); } From c2bb8699f8893ef637072b5054ce8cf9514b330f Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 9 Jan 2024 13:19:33 +0100 Subject: [PATCH 3/8] chore: Get all channels Extending the rust-dlc storage api to also be able to get non-signed channels. --- Cargo.lock | 10 +-- Cargo.toml | 10 +-- coordinator/src/admin.rs | 17 ++--- .../ln-dlc-node/src/ln/dlc_channel_details.rs | 65 +++++++++++++++---- crates/ln-dlc-node/src/node/dlc_channel.rs | 20 ++++-- crates/ln-dlc-storage/src/lib.rs | 9 +++ .../native/src/channel_trade_constraints.rs | 2 +- mobile/native/src/ln_dlc/channel_status.rs | 2 +- mobile/native/src/ln_dlc/mod.rs | 8 +-- 9 files changed, 96 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53bbf887e..63db5d7ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,7 +1092,7 @@ dependencies = [ [[package]] name = "dlc" version = "0.4.0" -source = "git+https://github.com/p2pderivatives/rust-dlc?rev=5373146#53731464629c6dfc3e162d3c48d5434589eaf2c3" +source = "git+https://github.com/p2pderivatives/rust-dlc?rev=9735996802f64c0776a6969fa8ef24fba43f8630#9735996802f64c0776a6969fa8ef24fba43f8630" dependencies = [ "bitcoin", "miniscript 8.0.2", @@ -1104,7 +1104,7 @@ dependencies = [ [[package]] name = "dlc-manager" version = "0.4.0" -source = "git+https://github.com/p2pderivatives/rust-dlc?rev=5373146#53731464629c6dfc3e162d3c48d5434589eaf2c3" +source = "git+https://github.com/p2pderivatives/rust-dlc?rev=9735996802f64c0776a6969fa8ef24fba43f8630#9735996802f64c0776a6969fa8ef24fba43f8630" dependencies = [ "async-trait", "bitcoin", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "dlc-messages" version = "0.4.0" -source = "git+https://github.com/p2pderivatives/rust-dlc?rev=5373146#53731464629c6dfc3e162d3c48d5434589eaf2c3" +source = "git+https://github.com/p2pderivatives/rust-dlc?rev=9735996802f64c0776a6969fa8ef24fba43f8630#9735996802f64c0776a6969fa8ef24fba43f8630" dependencies = [ "bitcoin", "dlc", @@ -1133,7 +1133,7 @@ dependencies = [ [[package]] name = "dlc-trie" version = "0.4.0" -source = "git+https://github.com/p2pderivatives/rust-dlc?rev=5373146#53731464629c6dfc3e162d3c48d5434589eaf2c3" +source = "git+https://github.com/p2pderivatives/rust-dlc?rev=9735996802f64c0776a6969fa8ef24fba43f8630#9735996802f64c0776a6969fa8ef24fba43f8630" dependencies = [ "bitcoin", "dlc", @@ -2584,7 +2584,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "p2pd-oracle-client" version = "0.1.0" -source = "git+https://github.com/p2pderivatives/rust-dlc?rev=5373146#53731464629c6dfc3e162d3c48d5434589eaf2c3" +source = "git+https://github.com/p2pderivatives/rust-dlc?rev=9735996802f64c0776a6969fa8ef24fba43f8630#9735996802f64c0776a6969fa8ef24fba43f8630" dependencies = [ "chrono", "dlc-manager", diff --git a/Cargo.toml b/Cargo.toml index ab4ce363f..b929f1dfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,11 +22,11 @@ resolver = "2" # `p2pderivatives/rust-dlc#feature/ln-dlc-channels`: 4e104b4. This patch ensures backwards # compatibility for 10101 through the `rust-lightning:0.0.116` upgrade. We will be able to drop it # once all users have been upgraded and traded once. -dlc-manager = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "5373146" } -dlc-messages = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "5373146" } -dlc = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "5373146" } -p2pd-oracle-client = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "5373146" } -dlc-trie = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "5373146" } +dlc-manager = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "9735996802f64c0776a6969fa8ef24fba43f8630" } +dlc-messages = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "9735996802f64c0776a6969fa8ef24fba43f8630" } +dlc = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "9735996802f64c0776a6969fa8ef24fba43f8630" } +p2pd-oracle-client = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "9735996802f64c0776a6969fa8ef24fba43f8630" } +dlc-trie = { git = "https://github.com/p2pderivatives/rust-dlc", rev = "9735996802f64c0776a6969fa8ef24fba43f8630" } # We should usually track the `p2pderivatives/split-tx-experiment[-10101]` branch. lightning = { git = "https://github.com/p2pderivatives/rust-lightning/", rev = "121bc324" } diff --git a/coordinator/src/admin.rs b/coordinator/src/admin.rs index b403d56b7..7ce61c418 100644 --- a/coordinator/src/admin.rs +++ b/coordinator/src/admin.rs @@ -15,7 +15,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; use commons::CollaborativeRevertCoordinatorExpertRequest; use commons::CollaborativeRevertCoordinatorRequest; -use dlc_manager::channel::signed_channel::SignedChannel; +use dlc_manager::channel::Channel; use dlc_manager::contract::Contract; use lightning_invoice::Bolt11Invoice; use ln_dlc_node::node::NodeInfo; @@ -246,17 +246,10 @@ pub struct DlcChannelDetails { pub user_registration_timestamp: Option, } -impl - From<( - SignedChannel, - Option, - String, - Option, - )> for DlcChannelDetails -{ +impl From<(Channel, Option, String, Option)> for DlcChannelDetails { fn from( (channel_details, contract, user_email, user_registration_timestamp): ( - SignedChannel, + Channel, Option, String, Option, @@ -288,12 +281,12 @@ pub async fn list_dlc_channels( .into_iter() .map(|dlc_channel| { let (email, registration_timestamp) = - match db::user::by_id(&mut conn, dlc_channel.counter_party.to_string()) { + match db::user::by_id(&mut conn, dlc_channel.get_counter_party_id().to_string()) { Ok(Some(user)) => (user.email, Some(user.timestamp)), _ => ("unknown".to_string(), None), }; - let dlc_channel_id = dlc_channel.channel_id; + let dlc_channel_id = dlc_channel.get_id(); let contract = match state .node diff --git a/crates/ln-dlc-node/src/ln/dlc_channel_details.rs b/crates/ln-dlc-node/src/ln/dlc_channel_details.rs index 03a46de09..0ec83e7c4 100644 --- a/crates/ln-dlc-node/src/ln/dlc_channel_details.rs +++ b/crates/ln-dlc-node/src/ln/dlc_channel_details.rs @@ -1,8 +1,7 @@ use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; -use dlc_manager::channel::signed_channel::SignedChannel; +use dlc_manager::channel::Channel; use dlc_manager::DlcChannelId; -use lightning::ln::ChannelId; use serde::Serialize; use serde::Serializer; @@ -12,9 +11,10 @@ pub struct DlcChannelDetails { pub dlc_channel_id: Option, #[serde(serialize_with = "pk_as_hex")] pub counter_party: PublicKey, - pub update_idx: u64, - pub subchannel_state: SignedChannelState, - pub fee_rate_per_vb: u64, + pub channel_state: ChannelState, + pub signed_channel_state: Option, + pub update_idx: Option, + pub fee_rate_per_vb: Option, } #[derive(Serialize, Debug)] @@ -33,14 +33,55 @@ pub enum SignedChannelState { CollaborativeCloseOffered, } -impl From for DlcChannelDetails { - fn from(sc: SignedChannel) -> Self { +#[derive(Serialize, Debug)] +pub enum ChannelState { + Offered, + Accepted, + Signed, + Closing, + Closed, + CounterClosed, + ClosedPunished, + CollaborativelyClosed, + FailedAccept, + FailedSign, +} + +impl From for DlcChannelDetails { + fn from(channel: Channel) -> Self { + let (update_idx, state, fee_rate_per_vb) = match channel.clone() { + Channel::Signed(signed_channel) => ( + Some(signed_channel.update_idx), + Some(SignedChannelState::from(signed_channel.state)), + Some(signed_channel.fee_rate_per_vb), + ), + _ => (None, None, None), + }; + DlcChannelDetails { - dlc_channel_id: Some(sc.channel_id), - counter_party: sc.counter_party, - update_idx: sc.update_idx, - subchannel_state: SignedChannelState::from(sc.state), - fee_rate_per_vb: sc.fee_rate_per_vb, + dlc_channel_id: Some(channel.get_id()), + counter_party: channel.get_counter_party_id(), + channel_state: ChannelState::from(channel), + signed_channel_state: state.map(SignedChannelState::from), + update_idx, + fee_rate_per_vb, + } + } +} + +impl From for ChannelState { + fn from(value: Channel) -> Self { + match value { + Channel::Offered(_) => ChannelState::Offered, + Channel::Accepted(_) => ChannelState::Accepted, + Channel::Signed(_) => ChannelState::Signed, + Channel::Closing(_) => ChannelState::Closing, + Channel::Closed(_) => ChannelState::Closed, + Channel::CounterClosed(_) => ChannelState::CounterClosed, + Channel::ClosedPunished(_) => ChannelState::ClosedPunished, + Channel::CollaborativelyClosed(_) => ChannelState::CollaborativelyClosed, + Channel::FailedAccept(_) => ChannelState::FailedAccept, + Channel::FailedSign(_) => ChannelState::FailedSign, } } } diff --git a/crates/ln-dlc-node/src/node/dlc_channel.rs b/crates/ln-dlc-node/src/node/dlc_channel.rs index b004c4cf5..5f89bdf1d 100644 --- a/crates/ln-dlc-node/src/node/dlc_channel.rs +++ b/crates/ln-dlc-node/src/node/dlc_channel.rs @@ -39,7 +39,7 @@ impl Nod ); if let Some(channel) = self - .list_dlc_channels()? + .list_signed_dlc_channels()? .iter() .find(|channel| channel.counter_party == counterparty) { @@ -163,7 +163,7 @@ impl Nod tracing::info!(channel_id = channel_id_hex, "Closing DLC channel"); let channel = self - .get_dlc_channel(|channel| channel.channel_id == channel_id)? + .get_signed_dlc_channel(|channel| channel.channel_id == channel_id)? .context("DLC channel to close not found")?; if force { @@ -390,30 +390,36 @@ impl Nod dlc_channel.counter_party == *pubkey && matches!(&dlc_channel.state, SignedChannelState::Established { .. }) }; - let dlc_channel = self.get_dlc_channel(&matcher)?; + let dlc_channel = self.get_signed_dlc_channel(&matcher)?; Ok(dlc_channel) } - fn get_dlc_channel( + fn get_signed_dlc_channel( &self, matcher: impl FnMut(&&SignedChannel) -> bool, ) -> Result> { - let dlc_channels = self.list_dlc_channels()?; + let dlc_channels = self.list_signed_dlc_channels()?; let dlc_channel = dlc_channels.iter().find(matcher); Ok(dlc_channel.cloned()) } - pub fn list_dlc_channels(&self) -> Result> { + pub fn list_signed_dlc_channels(&self) -> Result> { let dlc_channels = self.dlc_manager.get_store().get_signed_channels(None)?; Ok(dlc_channels) } + pub fn list_dlc_channels(&self) -> Result> { + let dlc_channels = self.dlc_manager.get_store().get_channels()?; + + Ok(dlc_channels) + } + /// Returns the usable balance in all DLC channels. Usable means, the amount currently locked up /// in a position does not count to the balance pub fn get_usable_dlc_channel_balance(&self) -> Result { - let dlc_channels = self.list_dlc_channels()?; + let dlc_channels = self.list_signed_dlc_channels()?; Ok(Amount::from_sat( dlc_channels .iter() diff --git a/crates/ln-dlc-storage/src/lib.rs b/crates/ln-dlc-storage/src/lib.rs index c9e0387fd..6acfdaf19 100644 --- a/crates/ln-dlc-storage/src/lib.rs +++ b/crates/ln-dlc-storage/src/lib.rs @@ -558,6 +558,15 @@ impl dlc_manager::Storage for DlcStorageProvider { Ok(res) } + + fn get_channels(&self) -> Result, Error> { + self.store + .read(CHANNEL, None) + .map_err(to_storage_error)? + .iter() + .map(|c| deserialize_channel(&c.value)) + .collect() + } } impl WalletStorage for DlcStorageProvider { diff --git a/mobile/native/src/channel_trade_constraints.rs b/mobile/native/src/channel_trade_constraints.rs index f9925ab44..55fc41f16 100644 --- a/mobile/native/src/channel_trade_constraints.rs +++ b/mobile/native/src/channel_trade_constraints.rs @@ -21,7 +21,7 @@ pub fn channel_trade_constraints() -> Result { .context("we need at least one liquidity option")?; let coordinator_leverage = option.coordinator_leverage; - let dlc_channels = ln_dlc::get_dlc_channels()?; + let dlc_channels = ln_dlc::get_signed_dlc_channels()?; let maybe_channel = dlc_channels.first(); diff --git a/mobile/native/src/ln_dlc/channel_status.rs b/mobile/native/src/ln_dlc/channel_status.rs index 171cd7bb0..ed8906c09 100644 --- a/mobile/native/src/ln_dlc/channel_status.rs +++ b/mobile/native/src/ln_dlc/channel_status.rs @@ -59,7 +59,7 @@ async fn channel_status(node: impl Borrow) -> Result { let node: &Node = node.borrow(); let node = &node.inner; - let dlc_channels = node.list_dlc_channels()?; + let dlc_channels = node.list_signed_dlc_channels()?; if dlc_channels.len() > 1 { tracing::warn!( channels = dlc_channels.len(), diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 7fe94f831..436a7fdf2 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -490,7 +490,7 @@ fn keep_wallet_balance_and_history_up_to_date(node: &Node) -> Result<()> { // Get all channel related transactions (channel opening/closing). If we have seen a transaction // in the wallet it means the transaction has been published. If so, we remove it from // [`on_chain`] and add it as it's own WalletHistoryItem so that it can be displayed nicely. - let dlc_channels = node.inner.list_dlc_channels()?; + let dlc_channels = node.inner.list_signed_dlc_channels()?; let dlc_channel_funding_tx_details = on_chain.iter().filter_map(|details| { match dlc_channels @@ -715,7 +715,7 @@ pub async fn close_channel(is_force_close: bool) -> Result<()> { tracing::info!(force = is_force_close, "Offering to close a channel"); let node = state::try_get_node().context("failed to get ln dlc node")?; - let channels = node.inner.list_dlc_channels()?; + let channels = node.inner.list_signed_dlc_channels()?; let channel_details = channels.first().context("No channel to close")?; node.inner @@ -723,9 +723,9 @@ pub async fn close_channel(is_force_close: bool) -> Result<()> { .await } -pub fn get_dlc_channels() -> Result> { +pub fn get_signed_dlc_channels() -> Result> { let node = state::try_get_node().context("failed to get ln dlc node")?; - node.inner.list_dlc_channels() + node.inner.list_signed_dlc_channels() } pub fn get_onchain_balance() -> Result { From 207f5c5b6a635bdcf54a61f3fe1c3324952da82a Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Mon, 15 Jan 2024 09:59:30 +0100 Subject: [PATCH 4/8] feat: Add basic node event handler Publishes an event if a peer connected successfully using the onion message handler hook. --- coordinator/src/bin/coordinator.rs | 2 + crates/ln-dlc-node/src/lib.rs | 4 +- crates/ln-dlc-node/src/node/connection.rs | 56 +++++++++++++++++++++++ crates/ln-dlc-node/src/node/event.rs | 40 ++++++++++++++++ crates/ln-dlc-node/src/node/mod.rs | 13 +++++- crates/ln-dlc-node/src/tests/mod.rs | 2 + maker/src/bin/maker.rs | 2 + mobile/native/src/ln_dlc/mod.rs | 3 ++ 8 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 crates/ln-dlc-node/src/node/event.rs diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index 94d5ce842..0749c4846 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -110,6 +110,7 @@ async fn main() -> Result<()> { let node_storage = Arc::new(NodeStorage::new(pool.clone())); + let node_event_handler = Arc::new(NodeEventHandler::new()); let node = Arc::new(ln_dlc_node::node::Node::new( ln_dlc_node::config::coordinator_config(), scorer::persistent_scorer, @@ -135,6 +136,7 @@ async fn main() -> Result<()> { .map(|o| o.into()) .collect(), XOnlyPublicKey::from_str(&opts.oracle_pubkey).expect("valid public key"), + node_event_handler.clone(), )?); let event_handler = CoordinatorEventHandler::new(node.clone(), Some(node_event_sender)); diff --git a/crates/ln-dlc-node/src/lib.rs b/crates/ln-dlc-node/src/lib.rs index 05b960755..944ab44d6 100644 --- a/crates/ln-dlc-node/src/lib.rs +++ b/crates/ln-dlc-node/src/lib.rs @@ -1,5 +1,6 @@ use crate::ln::TracingLogger; use crate::node::SubChannelManager; +use crate::node::TenTenOneOnionMessageHandler; use bitcoin::hashes::hex::ToHex; use bitcoin::Txid; use dlc_custom_signer::CustomKeysManager; @@ -9,7 +10,6 @@ use fee_rate_estimator::FeeRateEstimator; use lightning::chain::chainmonitor; use lightning::chain::Filter; use lightning::ln::msgs::RoutingMessageHandler; -use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::PaymentPreimage; use lightning::ln::PaymentSecret; use lightning::routing::gossip; @@ -71,7 +71,7 @@ pub type PeerManager = lightning::ln::peer_handler::PeerManager< SocketDescriptor, Arc>, Arc, - Arc, + Arc, Arc, Arc, Arc>, diff --git a/crates/ln-dlc-node/src/node/connection.rs b/crates/ln-dlc-node/src/node/connection.rs index 29e6a6dda..d91b12395 100644 --- a/crates/ln-dlc-node/src/node/connection.rs +++ b/crates/ln-dlc-node/src/node/connection.rs @@ -1,3 +1,5 @@ +use crate::node::event::NodeEvent; +use crate::node::event::NodeEventHandler; use crate::node::Node; use crate::node::NodeInfo; use crate::node::Storage; @@ -7,9 +9,63 @@ use anyhow::Context; use anyhow::Result; use bitcoin::secp256k1::PublicKey; use futures::Future; +use lightning::events::OnionMessageProvider; +use lightning::ln::features::InitFeatures; +use lightning::ln::features::NodeFeatures; +use lightning::ln::msgs; +use lightning::ln::msgs::OnionMessage; +use lightning::ln::msgs::OnionMessageHandler; use std::pin::Pin; +use std::sync::Arc; use std::time::Duration; +pub struct TenTenOneOnionMessageHandler { + handler: Arc, +} + +impl TenTenOneOnionMessageHandler { + pub fn new(handler: Arc) -> Self { + TenTenOneOnionMessageHandler { handler } + } +} + +/// Copied from the IgnoringMessageHandler +impl OnionMessageProvider for TenTenOneOnionMessageHandler { + fn next_onion_message_for_peer(&self, _peer_node_id: PublicKey) -> Option { + None + } +} + +/// Copied primarily from the IgnoringMessageHandler. Using the peer_connected hook to get notified +/// once a peer successfully connected. (This also includes that the Init Message has been processed +/// and the connection is ready to use). +impl OnionMessageHandler for TenTenOneOnionMessageHandler { + fn handle_onion_message(&self, _their_node_id: &PublicKey, _msg: &OnionMessage) {} + fn peer_connected( + &self, + their_node_id: &PublicKey, + _init: &msgs::Init, + inbound: bool, + ) -> Result<(), ()> { + tracing::info!(%their_node_id, inbound, "Peer connected!"); + + if let Err(e) = self.handler.publish(NodeEvent::Connected { + peer: *their_node_id, + }) { + tracing::error!(%their_node_id, "Failed to broadcast connected peer. {e:#}"); + } + + Ok(()) + } + fn peer_disconnected(&self, _their_node_id: &PublicKey) {} + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _their_node_id: &PublicKey) -> InitFeatures { + InitFeatures::empty() + } +} + impl Node { pub async fn connect(&self, peer: NodeInfo) -> Result>>> { #[allow(clippy::async_yields_async)] // We want to poll this future in a loop elsewhere diff --git a/crates/ln-dlc-node/src/node/event.rs b/crates/ln-dlc-node/src/node/event.rs new file mode 100644 index 000000000..203e332c3 --- /dev/null +++ b/crates/ln-dlc-node/src/node/event.rs @@ -0,0 +1,40 @@ +use anyhow::anyhow; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use dlc_messages::Message; +use tokio::sync::broadcast; +use tokio::sync::broadcast::Receiver; + +#[derive(Clone, Debug)] +pub enum NodeEvent { + Connected { peer: PublicKey }, +} + +#[derive(Clone)] +pub struct NodeEventHandler { + sender: broadcast::Sender, +} + +impl Default for NodeEventHandler { + fn default() -> Self { + Self::new() + } +} + +impl NodeEventHandler { + pub fn new() -> Self { + let (sender, _) = broadcast::channel(100); + + NodeEventHandler { sender } + } + + pub fn subscribe(&self) -> Receiver { + self.sender.subscribe() + } + + pub fn publish(&self, event: NodeEvent) -> Result<()> { + self.sender.send(event).map_err(|e| anyhow!("{e:#}"))?; + + Ok(()) + } +} diff --git a/crates/ln-dlc-node/src/node/mod.rs b/crates/ln-dlc-node/src/node/mod.rs index 3ab8b9bef..50b951677 100644 --- a/crates/ln-dlc-node/src/node/mod.rs +++ b/crates/ln-dlc-node/src/node/mod.rs @@ -81,10 +81,13 @@ pub(crate) mod invoice; pub(crate) mod sub_channel; pub mod dlc_channel; +pub mod event; pub mod peer_manager; +pub use crate::node::connection::TenTenOneOnionMessageHandler; pub use crate::node::dlc_manager::signed_channel_state_name; pub use crate::node::dlc_manager::DlcManager; +use crate::node::event::NodeEventHandler; pub use crate::node::oracle::OracleInfo; pub use ::dlc_manager as rust_dlc_manager; pub use channel_manager::ChannelManager; @@ -165,6 +168,8 @@ pub struct Node { /// The oracle pubkey used for proposing dlc channels pub oracle_pubkey: XOnlyPublicKey, + pub event_handler: Arc, + // storage // TODO(holzeis): The node storage should get extracted to the corresponding application // layers. @@ -275,6 +280,7 @@ impl Node, oracle_pubkey: XOnlyPublicKey, + node_event_handler: Arc, ) -> Result where SC: Fn(&Path, Arc, Arc) -> Scorer, @@ -440,10 +446,14 @@ impl Node Node { WalletSettings::default(), vec![oracle.into()], XOnlyPublicKey::from_str(ORACLE_PUBKEY)?, + Arc::new(NodeEventHandler::new()), )?; let node = Arc::new(node); diff --git a/maker/src/bin/maker.rs b/maker/src/bin/maker.rs index d09e59246..5b89fcd66 100644 --- a/maker/src/bin/maker.rs +++ b/maker/src/bin/maker.rs @@ -4,6 +4,7 @@ use bitcoin::Network; use diesel::r2d2; use diesel::r2d2::ConnectionManager; use diesel::PgConnection; +use ln_dlc_node::node::event::NodeEventHandler; use ln_dlc_node::node::InMemoryStore; use ln_dlc_node::seed::Bip39Seed; use ln_dlc_node::WalletSettings; @@ -103,6 +104,7 @@ async fn main() -> Result<()> { WalletSettings::default(), vec![opts.get_oracle_info().into()], opts.get_oracle_info().public_key, + Arc::new(NodeEventHandler::new()), )?); let event_handler = EventHandler::new(node.clone()); diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 436a7fdf2..2371ae296 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -106,6 +106,7 @@ pub mod channel_status; use crate::storage::TenTenOneNodeStorage; pub use channel_status::ChannelStatus; +use ln_dlc_node::node::event::NodeEventHandler; use ln_dlc_node::node::rust_dlc_manager::channel::signed_channel::SignedChannel; const PROCESS_INCOMING_DLC_MESSAGES_INTERVAL: Duration = Duration::from_millis(200); @@ -298,6 +299,7 @@ pub fn run(seed_dir: String, runtime: &Runtime) -> Result<()> { event::subscribe(DBBackupSubscriber::new(storage.clone().client)); + let node_event_handler = Arc::new(NodeEventHandler::new()); let node = ln_dlc_node::node::Node::new( app_config(), scorer::in_memory_scorer, @@ -316,6 +318,7 @@ pub fn run(seed_dir: String, runtime: &Runtime) -> Result<()> { WalletSettings::default(), vec![config::get_oracle_info().into()], config::get_oracle_info().public_key, + node_event_handler.clone(), )?; let node = Arc::new(node); From 015c36dd7b19fd780bda51f4fb31780b72f45ef8 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Mon, 15 Jan 2024 13:26:50 +0100 Subject: [PATCH 5/8] feat: Resend last outbound dlc messages on connect Upon connecting to the peer, the last send dlc message is looked up from the database (`last_outbvound_dlc_messages`) and is naively resend to the counterparty (this is applicable for both sides). Once the counterparty receives the last message, it will check if it has already processed that message looking up the hash of the dlc message in the `dlc_messages` table. If it has alrady seen the message it will simply ignore it if not, it will continue processing it as it appears it must have missed it the first time, thus continuing an interrupted protocol. - All dlc messages are stored into the `dlc_messages` table once successfully processed - The last outbound dlc message is stored to the `last_outbound_dlc_messages` table. Note, that since a dlc message can get quite large we are only keeping the last message around so to not waste space. In order to keep track of all outbound dlc messages I had to introduce the `SendDlcMessage` Event, which re-routes the sending over the app / coordinator layer who is then able to store the message firstly into the `dlc_messages` and `last_outbound_dlc_messages` before actually sending the message to the counter party. This way we ensure we are always able to resend the last message. Note, since we are using different data sources with independet transactional systems it can happen that we are persting things into the sled database (dlc), but not into the sqlite database (10101). This should get addressed eventually. e.g. by also storing the dlc data structures into the same sqlite database. --- CHANGELOG.md | 1 + .../down.sql | 7 + .../2024-01-10-105231_add_dlc_messages/up.sql | 39 +++ coordinator/src/bin/coordinator.rs | 12 + coordinator/src/db/custom_types.rs | 68 +++++ coordinator/src/db/dlc_messages.rs | 209 +++++++++++++++ .../src/db/last_outbound_dlc_message.rs | 97 +++++++ coordinator/src/db/mod.rs | 2 + coordinator/src/dlc_handler.rs | 171 ++++++++++++ coordinator/src/lib.rs | 1 + coordinator/src/node.rs | 45 +++- coordinator/src/schema.rs | 35 +++ crates/ln-dlc-node/src/dlc_message.rs | 245 ++++++++++++++++++ crates/ln-dlc-node/src/lib.rs | 1 + crates/ln-dlc-node/src/node/dlc_channel.rs | 81 +++--- crates/ln-dlc-node/src/node/event.rs | 1 + crates/ln-dlc-node/src/tests/mod.rs | 31 ++- .../down.sql | 3 + .../2024-01-10-105231_add_dlc_messages/up.sql | 17 ++ mobile/native/src/db/custom_types.rs | 72 +++++ mobile/native/src/db/dlc_messages.rs | 195 ++++++++++++++ .../src/db/last_outbound_dlc_messages.rs | 99 +++++++ mobile/native/src/db/mod.rs | 2 + mobile/native/src/dlc_handler.rs | 158 +++++++++++ mobile/native/src/lib.rs | 1 + mobile/native/src/ln_dlc/mod.rs | 12 + mobile/native/src/ln_dlc/node.rs | 134 ++++++---- mobile/native/src/schema.rs | 24 ++ 28 files changed, 1661 insertions(+), 102 deletions(-) create mode 100644 coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql create mode 100644 coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql create mode 100644 coordinator/src/db/dlc_messages.rs create mode 100644 coordinator/src/db/last_outbound_dlc_message.rs create mode 100644 coordinator/src/dlc_handler.rs create mode 100644 crates/ln-dlc-node/src/dlc_message.rs create mode 100644 mobile/native/migrations/2024-01-10-105231_add_dlc_messages/down.sql create mode 100644 mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql create mode 100644 mobile/native/src/db/dlc_messages.rs create mode 100644 mobile/native/src/db/last_outbound_dlc_messages.rs create mode 100644 mobile/native/src/dlc_handler.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index eb54071dc..05b7dbbe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Feat: allow force-close a DLC channel - Feat: made sure that rollover works with dlc-channels - Fix: correctly remember reserved utxos and don't accidentally double spend +- Feat: Allow recovering from a stuck protocol state by resending last outbound dlc message on connect ## [1.7.4] - 2023-12-20 diff --git a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql new file mode 100644 index 000000000..c5f672274 --- /dev/null +++ b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +DROP TABLE "last_outbound_dlc_messages"; +DROP TABLE "dlc_messages"; + +DROP TYPE "Message_Sub_Type_Type"; +DROP TYPE "Message_Type_Type"; + diff --git a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql new file mode 100644 index 000000000..4d52890e7 --- /dev/null +++ b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -0,0 +1,39 @@ +CREATE TYPE "Message_Type_Type" AS ENUM ( + 'OnChain', + 'Channel' +); + +CREATE TYPE "Message_Sub_Type_Type" AS ENUM ( + 'Offer', + 'Accept', + 'Sign', + 'SettleOffer', + 'SettleAccept', + 'SettleConfirm', + 'SettleFinalize', + 'RenewOffer', + 'RenewAccept', + 'RenewConfirm', + 'RenewFinalize', + 'RenewRevoke', + 'CollaborativeCloseOffer', + 'Reject' +); + +CREATE TABLE "dlc_messages" ( + -- We need to store the hash as TEXT as the BIGINT type overflows on some u64 values breaking the hash value. + message_hash TEXT PRIMARY KEY NOT NULL, + inbound BOOLEAN NOT NULL, + peer_id TEXT NOT NULL, + message_type "Message_Type_Type" NOT NULL, + message_sub_type "Message_Sub_Type_Type" NOT NULL, + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE "last_outbound_dlc_messages" ( + peer_id TEXT PRIMARY KEY NOT NULL, + message_hash TEXT REFERENCES dlc_messages(message_hash) NOT NULL, + message TEXT NOT NULL, + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); + diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index 0749c4846..b048a77f0 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -3,6 +3,8 @@ use anyhow::Result; use bitcoin::XOnlyPublicKey; use coordinator::backup::SledBackup; use coordinator::cli::Opts; +use coordinator::dlc_handler; +use coordinator::dlc_handler::DlcHandler; use coordinator::logger; use coordinator::message::spawn_delivering_messages_to_authenticated_users; use coordinator::message::NewUserMessage; @@ -28,6 +30,7 @@ use diesel::r2d2; use diesel::r2d2::ConnectionManager; use diesel::PgConnection; use lightning::events::Event; +use ln_dlc_node::node::event::NodeEventHandler; use ln_dlc_node::scorer; use ln_dlc_node::seed::Bip39Seed; use ln_dlc_node::CoordinatorEventHandler; @@ -139,6 +142,15 @@ async fn main() -> Result<()> { node_event_handler.clone(), )?); + let dlc_handler = DlcHandler::new(pool.clone(), node.clone()); + let _handle = + // this handles sending outbound dlc messages as well as keeping track of what + // dlc messages have already been processed and what was the last outbound dlc message + // so it can be resend on reconnect. + // + // this does not handle the incoming dlc messages! + dlc_handler::spawn_handling_dlc_messages(dlc_handler, node_event_handler.subscribe()); + let event_handler = CoordinatorEventHandler::new(node.clone(), Some(node_event_sender)); let running = node.start(event_handler, false)?; let node = Node::new(node, running, pool.clone(), settings.to_node_settings()); diff --git a/coordinator/src/db/custom_types.rs b/coordinator/src/db/custom_types.rs index 87441d212..88c34bfdf 100644 --- a/coordinator/src/db/custom_types.rs +++ b/coordinator/src/db/custom_types.rs @@ -1,4 +1,6 @@ use crate::db::channels::ChannelState; +use crate::db::dlc_messages::MessageSubType; +use crate::db::dlc_messages::MessageType; use crate::db::payments::HtlcStatus; use crate::db::payments::PaymentFlow; use crate::db::positions::ContractSymbol; @@ -7,6 +9,8 @@ use crate::schema::sql_types::ChannelStateType; use crate::schema::sql_types::ContractSymbolType; use crate::schema::sql_types::DirectionType; use crate::schema::sql_types::HtlcStatusType; +use crate::schema::sql_types::MessageSubTypeType; +use crate::schema::sql_types::MessageTypeType; use crate::schema::sql_types::PaymentFlowType; use crate::schema::sql_types::PositionStateType; use diesel::deserialize; @@ -159,3 +163,67 @@ impl FromSql for Direction { } } } + +impl ToSql for MessageType { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { + match *self { + MessageType::OnChain => out.write_all(b"OnChain")?, + MessageType::Channel => out.write_all(b"Channel")?, + } + Ok(IsNull::No) + } +} + +impl FromSql for MessageType { + fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { + match bytes.as_bytes() { + b"OnChain" => Ok(MessageType::OnChain), + b"Channel" => Ok(MessageType::Channel), + _ => Err("Unrecognized enum variant".into()), + } + } +} + +impl ToSql for MessageSubType { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { + match *self { + MessageSubType::Offer => out.write_all(b"Offer")?, + MessageSubType::Accept => out.write_all(b"Accept")?, + MessageSubType::Sign => out.write_all(b"Sign")?, + MessageSubType::SettleOffer => out.write_all(b"SettleOffer")?, + MessageSubType::SettleAccept => out.write_all(b"SettleAccept")?, + MessageSubType::SettleConfirm => out.write_all(b"SettleConfirm")?, + MessageSubType::SettleFinalize => out.write_all(b"SettleFinalize")?, + MessageSubType::RenewOffer => out.write_all(b"RenewOffer")?, + MessageSubType::RenewAccept => out.write_all(b"RenewAccept")?, + MessageSubType::RenewConfirm => out.write_all(b"RenewConfirm")?, + MessageSubType::RenewFinalize => out.write_all(b"RenewFinalize")?, + MessageSubType::RenewRevoke => out.write_all(b"RenewRevoke")?, + MessageSubType::CollaborativeCloseOffer => out.write_all(b"CollaborativeCloseOffer")?, + MessageSubType::Reject => out.write_all(b"Reject")?, + } + Ok(IsNull::No) + } +} + +impl FromSql for MessageSubType { + fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { + match bytes.as_bytes() { + b"Offer" => Ok(MessageSubType::Offer), + b"Accept" => Ok(MessageSubType::Accept), + b"Sign" => Ok(MessageSubType::Sign), + b"SettleOffer" => Ok(MessageSubType::SettleOffer), + b"SettleAccept" => Ok(MessageSubType::SettleAccept), + b"SettleConfirm" => Ok(MessageSubType::SettleConfirm), + b"SettleFinalize" => Ok(MessageSubType::SettleFinalize), + b"RenewOffer" => Ok(MessageSubType::RenewOffer), + b"RenewAccept" => Ok(MessageSubType::RenewAccept), + b"RenewConfirm" => Ok(MessageSubType::RenewConfirm), + b"RenewFinalize" => Ok(MessageSubType::RenewFinalize), + b"RenewRevoke" => Ok(MessageSubType::RenewRevoke), + b"CollaborativeCloseOffer" => Ok(MessageSubType::CollaborativeCloseOffer), + b"Reject" => Ok(MessageSubType::Reject), + _ => Err("Unrecognized enum variant".into()), + } + } +} diff --git a/coordinator/src/db/dlc_messages.rs b/coordinator/src/db/dlc_messages.rs new file mode 100644 index 000000000..9250aab60 --- /dev/null +++ b/coordinator/src/db/dlc_messages.rs @@ -0,0 +1,209 @@ +use crate::schema; +use crate::schema::dlc_messages; +use crate::schema::sql_types::MessageSubTypeType; +use crate::schema::sql_types::MessageTypeType; +use anyhow::ensure; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::query_builder::QueryId; +use diesel::AsChangeset; +use diesel::AsExpression; +use diesel::ExpressionMethods; +use diesel::FromSqlRow; +use diesel::Insertable; +use diesel::OptionalExtension; +use diesel::PgConnection; +use diesel::QueryDsl; +use diesel::QueryResult; +use diesel::Queryable; +use diesel::QueryableByName; +use diesel::RunQueryDsl; +use std::any::TypeId; +use std::str::FromStr; +use time::OffsetDateTime; + +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = MessageTypeType)] +pub(crate) enum MessageType { + OnChain, + Channel, +} + +impl QueryId for MessageTypeType { + type QueryId = MessageTypeType; + const HAS_STATIC_QUERY_ID: bool = false; + + fn query_id() -> Option { + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = MessageSubTypeType)] +pub(crate) enum MessageSubType { + Offer, + Accept, + Sign, + SettleOffer, + SettleAccept, + SettleConfirm, + SettleFinalize, + RenewOffer, + RenewAccept, + RenewConfirm, + RenewFinalize, + RenewRevoke, + CollaborativeCloseOffer, + Reject, +} + +impl QueryId for MessageSubTypeType { + type QueryId = MessageSubTypeType; + const HAS_STATIC_QUERY_ID: bool = false; + + fn query_id() -> Option { + None + } +} + +#[derive(Insertable, QueryableByName, Queryable, Debug, Clone, PartialEq, AsChangeset)] +#[diesel(table_name = dlc_messages)] +pub(crate) struct DlcMessage { + pub message_hash: String, + pub inbound: bool, + pub peer_id: String, + pub message_type: MessageType, + pub message_sub_type: MessageSubType, + pub timestamp: OffsetDateTime, +} + +pub(crate) fn get(conn: &mut PgConnection, message_hash: u64) -> QueryResult> { + dlc_messages::table + .filter(dlc_messages::message_hash.eq(message_hash.to_string())) + .first::(conn) + .optional() +} + +pub(crate) fn insert( + conn: &mut PgConnection, + dlc_message: ln_dlc_node::dlc_message::DlcMessage, +) -> Result<()> { + let affected_rows = diesel::insert_into(schema::dlc_messages::table) + .values(DlcMessage::from(dlc_message)) + .execute(conn)?; + + ensure!(affected_rows > 0, "Could not insert dlc message"); + + Ok(()) +} + +impl From for DlcMessage { + fn from(value: ln_dlc_node::dlc_message::DlcMessage) -> Self { + Self { + message_hash: value.message_hash.to_string(), + peer_id: value.peer_id.to_string(), + message_type: MessageType::from(value.clone().message_type), + message_sub_type: MessageSubType::from(value.message_type), + timestamp: value.timestamp, + inbound: value.inbound, + } + } +} + +impl From for MessageType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { + match value { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(_) => Self::OnChain, + ln_dlc_node::dlc_message::DlcMessageType::Channel(_) => Self::Channel, + } + } +} + +impl From for MessageSubType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { + let message_sub_type = match value { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(message_sub_type) => message_sub_type, + ln_dlc_node::dlc_message::DlcMessageType::Channel(message_sub_type) => message_sub_type, + }; + MessageSubType::from(message_sub_type) + } +} + +impl From for MessageSubType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageSubType) -> Self { + match value { + ln_dlc_node::dlc_message::DlcMessageSubType::Offer => Self::Offer, + ln_dlc_node::dlc_message::DlcMessageSubType::Accept => Self::Accept, + ln_dlc_node::dlc_message::DlcMessageSubType::Sign => Self::Sign, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer => Self::SettleOffer, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept => Self::SettleAccept, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm => Self::SettleConfirm, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize => Self::SettleFinalize, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer => Self::RenewOffer, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept => Self::RenewAccept, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm => Self::RenewConfirm, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize => Self::RenewFinalize, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke => Self::RenewRevoke, + ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer => { + Self::CollaborativeCloseOffer + } + ln_dlc_node::dlc_message::DlcMessageSubType::Reject => Self::Reject, + } + } +} + +impl From for ln_dlc_node::dlc_message::DlcMessage { + fn from(value: DlcMessage) -> Self { + let dlc_message_sub_type = + ln_dlc_node::dlc_message::DlcMessageSubType::from(value.clone().message_sub_type); + let dlc_message_type = match &value.message_type { + MessageType::OnChain => { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) + } + MessageType::Channel => { + ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) + } + }; + + Self { + message_hash: u64::from_str(&value.message_hash).expect("valid u64"), + inbound: value.inbound, + message_type: dlc_message_type, + peer_id: PublicKey::from_str(&value.peer_id).expect("valid public key"), + timestamp: value.timestamp, + } + } +} + +impl From for ln_dlc_node::dlc_message::DlcMessageSubType { + fn from(value: MessageSubType) -> Self { + match value { + MessageSubType::Offer => ln_dlc_node::dlc_message::DlcMessageSubType::Offer, + MessageSubType::Accept => ln_dlc_node::dlc_message::DlcMessageSubType::Accept, + MessageSubType::Sign => ln_dlc_node::dlc_message::DlcMessageSubType::Sign, + MessageSubType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer, + MessageSubType::SettleAccept => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept + } + MessageSubType::SettleConfirm => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm + } + MessageSubType::SettleFinalize => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize + } + MessageSubType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer, + MessageSubType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept, + MessageSubType::RenewConfirm => { + ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm + } + MessageSubType::RenewFinalize => { + ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize + } + MessageSubType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke, + MessageSubType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer + } + MessageSubType::Reject => ln_dlc_node::dlc_message::DlcMessageSubType::Reject, + } + } +} diff --git a/coordinator/src/db/last_outbound_dlc_message.rs b/coordinator/src/db/last_outbound_dlc_message.rs new file mode 100644 index 000000000..078defbbe --- /dev/null +++ b/coordinator/src/db/last_outbound_dlc_message.rs @@ -0,0 +1,97 @@ +use crate::db::dlc_messages::MessageSubType; +use crate::db::dlc_messages::MessageType; +use crate::schema; +use crate::schema::dlc_messages; +use crate::schema::last_outbound_dlc_messages; +use anyhow::ensure; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::AsChangeset; +use diesel::ExpressionMethods; +use diesel::Insertable; +use diesel::JoinOnDsl; +use diesel::OptionalExtension; +use diesel::PgConnection; +use diesel::QueryDsl; +use diesel::QueryResult; +use diesel::Queryable; +use diesel::QueryableByName; +use diesel::RunQueryDsl; +use ln_dlc_node::dlc_message::SerializedDlcMessage; +use time::OffsetDateTime; + +#[derive(Insertable, QueryableByName, Queryable, Debug, Clone, PartialEq, AsChangeset)] +#[diesel(table_name = last_outbound_dlc_messages)] +pub(crate) struct LastOutboundDlcMessage { + pub peer_id: String, + pub message_hash: String, + pub message: String, + pub timestamp: OffsetDateTime, +} + +pub(crate) fn get( + conn: &mut PgConnection, + peer_id: &PublicKey, +) -> QueryResult> { + let last_outbound_dlc_message = last_outbound_dlc_messages::table + .inner_join( + dlc_messages::table + .on(dlc_messages::message_hash.eq(last_outbound_dlc_messages::message_hash)), + ) + .filter(last_outbound_dlc_messages::peer_id.eq(peer_id.to_string())) + .select(( + dlc_messages::message_type, + dlc_messages::message_sub_type, + last_outbound_dlc_messages::message, + )) + .first::<(MessageType, MessageSubType, String)>(conn) + .optional()?; + + let serialized_dlc_message = match last_outbound_dlc_message { + Some((message_type, message_sub_type, message)) => { + let dlc_message_sub_type = + ln_dlc_node::dlc_message::DlcMessageSubType::from(message_sub_type); + let message_type = match &message_type { + MessageType::OnChain => { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) + } + MessageType::Channel => { + ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) + } + }; + + Some(SerializedDlcMessage { + message, + message_type, + }) + } + None => None, + }; + + Ok(serialized_dlc_message) +} + +pub(crate) fn upsert( + conn: &mut PgConnection, + peer_id: &PublicKey, + sdm: SerializedDlcMessage, +) -> Result<()> { + let values = ( + last_outbound_dlc_messages::peer_id.eq(peer_id.to_string()), + last_outbound_dlc_messages::message_hash.eq(sdm.generate_hash().to_string()), + last_outbound_dlc_messages::message.eq(sdm.message), + ); + let affected_rows = diesel::insert_into(last_outbound_dlc_messages::table) + .values(&values.clone()) + .on_conflict(schema::last_outbound_dlc_messages::peer_id) + .do_update() + .set(values) + .execute(conn)?; + + ensure!( + affected_rows > 0, + "Could not upsert last outbound dlc messages" + ); + + Ok(()) +} diff --git a/coordinator/src/db/mod.rs b/coordinator/src/db/mod.rs index bd82bb0b8..348f13a8f 100644 --- a/coordinator/src/db/mod.rs +++ b/coordinator/src/db/mod.rs @@ -1,6 +1,8 @@ pub mod channels; pub mod collaborative_reverts; pub mod custom_types; +pub mod dlc_messages; +pub mod last_outbound_dlc_message; pub mod liquidity; pub mod liquidity_options; pub mod payments; diff --git a/coordinator/src/dlc_handler.rs b/coordinator/src/dlc_handler.rs new file mode 100644 index 000000000..1faecad91 --- /dev/null +++ b/coordinator/src/dlc_handler.rs @@ -0,0 +1,171 @@ +use crate::db; +use crate::node::storage::NodeStorage; +use crate::storage::CoordinatorTenTenOneStorage; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::r2d2::ConnectionManager; +use diesel::r2d2::Pool; +use diesel::PgConnection; +use dlc_messages::Message; +use futures::future::RemoteHandle; +use futures::FutureExt; +use ln_dlc_node::dlc_message::DlcMessage; +use ln_dlc_node::dlc_message::SerializedDlcMessage; +use ln_dlc_node::node::dlc_channel::send_dlc_message; +use ln_dlc_node::node::event::NodeEvent; +use ln_dlc_node::node::Node; +use std::sync::Arc; +use tokio::sync::broadcast; +use tokio::sync::broadcast::error::RecvError; + +/// The DlcHandler is responsible for sending dlc messages and marking received ones as +/// processed. It's main purpose is to ensure the following. +/// +/// 1. Mark all received inbound messages as processed. +/// 2. Save the last outbound dlc message, so it can be resend on the next reconnect. +/// 3. Check if a receive message has already been processed and if so inform to skip the message. + +#[derive(Clone)] +pub struct DlcHandler { + node: Arc>, + pool: Pool>, +} + +impl DlcHandler { + pub fn new( + pool: Pool>, + node: Arc>, + ) -> Self { + DlcHandler { node, pool } + } +} + +pub fn spawn_handling_dlc_messages( + dlc_handler: DlcHandler, + mut receiver: broadcast::Receiver, +) -> RemoteHandle<()> { + let (fut, remote_handle) = async move { + loop { + match receiver.recv().await { + Ok(NodeEvent::Connected { peer }) => { + if let Err(e) = dlc_handler.on_connect(peer) { + tracing::error!(peer=%peer, "Failed to process on connect event. {e:#}"); + } + } + Ok(NodeEvent::SendDlcMessage { peer, msg }) => { + if let Err(e) = dlc_handler.send_dlc_message(peer, msg) { + tracing::error!(peer=%peer, "Failed to process end dlc message event. {e:#}"); + } + } + Err(RecvError::Lagged(skipped)) => { + tracing::warn!("Skipped {skipped} messages"); + } + Err(RecvError::Closed) => { + tracing::error!("Lost connection to sender!"); + break; + } + } + } + }.remote_handle(); + + tokio::spawn(fut); + + remote_handle +} + +impl DlcHandler { + pub fn send_dlc_message(&self, peer: PublicKey, msg: Message) -> Result<()> { + let mut conn = self.pool.get()?; + + let serialized_outbound_message = SerializedDlcMessage::try_from(&msg)?; + let outbound_msg = DlcMessage::new(peer, serialized_outbound_message.clone(), false)?; + + db::dlc_messages::insert(&mut conn, outbound_msg)?; + db::last_outbound_dlc_message::upsert(&mut conn, &peer, serialized_outbound_message)?; + + send_dlc_message( + &self.node.dlc_message_handler, + &self.node.peer_manager, + peer, + msg, + ); + + Ok(()) + } + + pub fn on_connect(&self, peer: PublicKey) -> Result<()> { + tracing::debug!(%peer, "Connected to peer, resending last dlc message!"); + + let mut conn = self.pool.get()?; + + let last_serialized_message = db::last_outbound_dlc_message::get(&mut conn, &peer)?; + + if let Some(last_serialized_message) = last_serialized_message { + tracing::debug!(%peer, ?last_serialized_message.message_type, "Sending last dlc message"); + + let message = Message::try_from(&last_serialized_message)?; + send_dlc_message( + &self.node.dlc_message_handler, + &self.node.peer_manager, + peer, + message, + ) + } else { + tracing::debug!(%peer, "No last dlc message found. Nothing todo."); + } + + Ok(()) + } + + // Returns either the dlc message step or return none, if the dlc message has already been + // processed. + pub fn start_dlc_message_step( + conn: &mut PgConnection, + msg: &Message, + peer_id: PublicKey, + ) -> Result> { + let serialized_inbound_message = SerializedDlcMessage::try_from(msg)?; + let inbound_msg = DlcMessage::new(peer_id, serialized_inbound_message, true)?; + + let dlc_message_step = match db::dlc_messages::get(conn, inbound_msg.message_hash)? { + Some(_) => None, // the dlc message has already been processed, no step necessary. + None => Some(DlcMessageStep { + peer_id, + inbound_msg, + }), + }; + + Ok(dlc_message_step) + } +} + +pub struct DlcMessageStep { + pub peer_id: PublicKey, + pub inbound_msg: DlcMessage, +} + +impl DlcMessageStep { + /// Finishes the current dlc step by storing the received inbound message as processed and + /// caching the last outbound dlc message (if any) into the database. + pub fn finish(&self, conn: &mut PgConnection, response: &Option) -> Result<()> { + tracing::debug!("Marking the received message as processed"); + + db::dlc_messages::insert(conn, self.inbound_msg.clone())?; + + if let Some(resp) = response { + tracing::debug!("Persisting last outbound dlc message"); + let serialized_outbound_message = SerializedDlcMessage::try_from(resp)?; + let outbound_msg = + DlcMessage::new(self.peer_id, serialized_outbound_message.clone(), false)?; + + db::dlc_messages::insert(conn, outbound_msg)?; + db::last_outbound_dlc_message::upsert( + conn, + &self.peer_id, + serialized_outbound_message, + )?; + } + + Ok(()) + } +} diff --git a/coordinator/src/lib.rs b/coordinator/src/lib.rs index ff9299ae6..8cafa2c0d 100644 --- a/coordinator/src/lib.rs +++ b/coordinator/src/lib.rs @@ -24,6 +24,7 @@ pub mod admin; pub mod backup; pub mod cli; pub mod db; +pub mod dlc_handler; pub mod logger; pub mod message; pub mod metrics; diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 7c29aa16c..51a0fce0d 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -1,6 +1,7 @@ use crate::compute_relative_contracts; use crate::db; use crate::decimal_from_f32; +use crate::dlc_handler::DlcHandler; use crate::node::storage::NodeStorage; use crate::orderbook::db::matches; use crate::orderbook::db::orders; @@ -599,16 +600,39 @@ impl Node { ); let resp = match &msg { - Message::OnChain(_) | Message::Channel(_) => self - .inner - .dlc_manager - .on_dlc_message(&msg, node_id) - .with_context(|| { - format!( - "Failed to handle {} dlc message from {node_id}", - dlc_message_name(&msg) - ) - })?, + Message::OnChain(_) | Message::Channel(_) => { + let dlc_message_step = { + let mut conn = self.pool.get()?; + let dlc_message_step = + DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; + + match dlc_message_step { + Some(dlc_message_step) => dlc_message_step, + None => { + tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); + return Ok(()); + } + } + }; + + let resp = self + .inner + .dlc_manager + .on_dlc_message(&msg, node_id) + .with_context(|| { + format!( + "Failed to handle {} dlc message from {node_id}", + dlc_message_name(&msg) + ) + })?; + + { + let mut conn = self.pool.get()?; + dlc_message_step.finish(&mut conn, &resp)?; + } + + resp + } Message::SubChannel(msg) => self .inner .sub_channel_manager @@ -625,6 +649,7 @@ impl Node { // TODO(holzeis): It would be nice if dlc messages are also propagated via events, so the // receiver can decide what events to process and we can skip this component specific logic // here. + // Note: The NodeEventHandler could be used for that! if let Message::Channel(ChannelMessage::RenewFinalize(r)) = &msg { self.finalize_rollover(&r.channel_id)?; } diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index aad226fbe..dd720b14c 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -21,6 +21,14 @@ pub mod sql_types { #[diesel(postgres_type(name = "MatchState_Type"))] pub struct MatchStateType; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "Message_Sub_Type_Type"))] + pub struct MessageSubTypeType; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "Message_Type_Type"))] + pub struct MessageTypeType; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "OrderReason_Type"))] pub struct OrderReasonType; @@ -76,6 +84,30 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::MessageTypeType; + use super::sql_types::MessageSubTypeType; + + dlc_messages (message_hash) { + message_hash -> Text, + inbound -> Bool, + peer_id -> Text, + message_type -> MessageTypeType, + message_sub_type -> MessageSubTypeType, + timestamp -> Timestamptz, + } +} + +diesel::table! { + last_outbound_dlc_messages (peer_id) { + peer_id -> Text, + message_hash -> Text, + message -> Text, + timestamp -> Timestamptz, + } +} + diesel::table! { liquidity_options (id) { id -> Int4, @@ -262,12 +294,15 @@ diesel::table! { } } +diesel::joinable!(last_outbound_dlc_messages -> dlc_messages (message_hash)); diesel::joinable!(liquidity_request_logs -> liquidity_options (liquidity_option)); diesel::joinable!(trades -> positions (position_id)); diesel::allow_tables_to_appear_in_same_query!( channels, collaborative_reverts, + dlc_messages, + last_outbound_dlc_messages, liquidity_options, liquidity_request_logs, matches, diff --git a/crates/ln-dlc-node/src/dlc_message.rs b/crates/ln-dlc-node/src/dlc_message.rs new file mode 100644 index 000000000..2b4c21665 --- /dev/null +++ b/crates/ln-dlc-node/src/dlc_message.rs @@ -0,0 +1,245 @@ +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use dlc_messages::ChannelMessage; +use dlc_messages::Message; +use dlc_messages::OnChainMessage; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; +use time::OffsetDateTime; +use ureq::serde_json; + +#[derive(Clone)] +pub struct DlcMessage { + pub message_hash: u64, + pub inbound: bool, + pub peer_id: PublicKey, + pub message_type: DlcMessageType, + pub timestamp: OffsetDateTime, +} + +impl DlcMessage { + pub fn new( + peer_id: PublicKey, + serialized_message: SerializedDlcMessage, + inbound: bool, + ) -> Result { + let message_hash = serialized_message.generate_hash(); + + Ok(Self { + message_hash, + inbound, + peer_id, + message_type: serialized_message.message_type, + timestamp: OffsetDateTime::now_utc(), + }) + } +} + +#[derive(Hash, Clone, Debug)] +pub struct SerializedDlcMessage { + pub message: String, + pub message_type: DlcMessageType, +} + +impl SerializedDlcMessage { + pub fn generate_hash(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } +} + +#[derive(Hash, Clone, Debug)] +pub enum DlcMessageType { + OnChain(DlcMessageSubType), + Channel(DlcMessageSubType), +} + +#[derive(Hash, Clone, Debug)] +pub enum DlcMessageSubType { + Offer, + Accept, + Sign, + SettleOffer, + SettleAccept, + SettleConfirm, + SettleFinalize, + RenewOffer, + RenewAccept, + RenewConfirm, + RenewFinalize, + RenewRevoke, + CollaborativeCloseOffer, + Reject, +} + +impl TryFrom<&SerializedDlcMessage> for Message { + type Error = anyhow::Error; + + fn try_from(serialized_msg: &SerializedDlcMessage) -> Result { + let message = match serialized_msg.clone().message_type { + DlcMessageType::OnChain(serialized_onchain_message_type) => { + match serialized_onchain_message_type { + DlcMessageSubType::Offer => Message::OnChain(OnChainMessage::Offer( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::Accept => Message::OnChain(OnChainMessage::Accept( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::Sign => Message::OnChain(OnChainMessage::Sign( + serde_json::from_str(&serialized_msg.message)?, + )), + _ => unreachable!(), + } + } + DlcMessageType::Channel(serialized_channel_message_type) => { + match serialized_channel_message_type { + DlcMessageSubType::Offer => Message::Channel(ChannelMessage::Offer( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::Accept => Message::Channel(ChannelMessage::Accept( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::Sign => Message::Channel(ChannelMessage::Sign( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::SettleOffer => Message::Channel( + ChannelMessage::SettleOffer(serde_json::from_str(&serialized_msg.message)?), + ), + DlcMessageSubType::SettleAccept => { + Message::Channel(ChannelMessage::SettleAccept(serde_json::from_str( + &serialized_msg.message, + )?)) + } + DlcMessageSubType::SettleConfirm => { + Message::Channel(ChannelMessage::SettleConfirm(serde_json::from_str( + &serialized_msg.message, + )?)) + } + DlcMessageSubType::SettleFinalize => { + Message::Channel(ChannelMessage::SettleFinalize(serde_json::from_str( + &serialized_msg.message, + )?)) + } + DlcMessageSubType::RenewOffer => Message::Channel(ChannelMessage::RenewOffer( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageSubType::RenewAccept => Message::Channel( + ChannelMessage::RenewAccept(serde_json::from_str(&serialized_msg.message)?), + ), + DlcMessageSubType::RenewConfirm => { + Message::Channel(ChannelMessage::RenewConfirm(serde_json::from_str( + &serialized_msg.message, + )?)) + } + DlcMessageSubType::RenewFinalize => { + Message::Channel(ChannelMessage::RenewFinalize(serde_json::from_str( + &serialized_msg.message, + )?)) + } + DlcMessageSubType::RenewRevoke => Message::Channel( + ChannelMessage::RenewRevoke(serde_json::from_str(&serialized_msg.message)?), + ), + DlcMessageSubType::CollaborativeCloseOffer => { + Message::Channel(ChannelMessage::CollaborativeCloseOffer( + serde_json::from_str(&serialized_msg.message)?, + )) + } + DlcMessageSubType::Reject => Message::Channel(ChannelMessage::Reject( + serde_json::from_str(&serialized_msg.message)?, + )), + } + } + }; + + Ok(message) + } +} + +impl TryFrom<&Message> for SerializedDlcMessage { + type Error = anyhow::Error; + + fn try_from(msg: &Message) -> Result { + let (message, message_type) = match &msg { + Message::OnChain(message) => match message { + OnChainMessage::Offer(offer) => ( + serde_json::to_string(&offer)?, + DlcMessageType::OnChain(DlcMessageSubType::Offer), + ), + OnChainMessage::Accept(accept) => ( + serde_json::to_string(&accept)?, + DlcMessageType::OnChain(DlcMessageSubType::Accept), + ), + OnChainMessage::Sign(sign) => ( + serde_json::to_string(&sign)?, + DlcMessageType::OnChain(DlcMessageSubType::Sign), + ), + }, + Message::Channel(message) => match message { + ChannelMessage::Offer(offer) => ( + serde_json::to_string(&offer)?, + DlcMessageType::Channel(DlcMessageSubType::Offer), + ), + ChannelMessage::Accept(accept) => ( + serde_json::to_string(&accept)?, + DlcMessageType::Channel(DlcMessageSubType::Accept), + ), + ChannelMessage::Sign(sign) => ( + serde_json::to_string(&sign)?, + DlcMessageType::Channel(DlcMessageSubType::Sign), + ), + ChannelMessage::SettleOffer(settle_offer) => ( + serde_json::to_string(&settle_offer)?, + DlcMessageType::Channel(DlcMessageSubType::SettleOffer), + ), + ChannelMessage::SettleAccept(settle_accept) => ( + serde_json::to_string(&settle_accept)?, + DlcMessageType::Channel(DlcMessageSubType::SettleAccept), + ), + ChannelMessage::SettleConfirm(settle_confirm) => ( + serde_json::to_string(&settle_confirm)?, + DlcMessageType::Channel(DlcMessageSubType::SettleConfirm), + ), + ChannelMessage::SettleFinalize(settle_finalize) => ( + serde_json::to_string(&settle_finalize)?, + DlcMessageType::Channel(DlcMessageSubType::SettleFinalize), + ), + ChannelMessage::RenewOffer(renew_offer) => ( + serde_json::to_string(&renew_offer)?, + DlcMessageType::Channel(DlcMessageSubType::RenewOffer), + ), + ChannelMessage::RenewAccept(renew_accept) => ( + serde_json::to_string(&renew_accept)?, + DlcMessageType::Channel(DlcMessageSubType::RenewAccept), + ), + ChannelMessage::RenewConfirm(renew_confirm) => ( + serde_json::to_string(&renew_confirm)?, + DlcMessageType::Channel(DlcMessageSubType::RenewConfirm), + ), + ChannelMessage::RenewFinalize(renew_finalize) => ( + serde_json::to_string(&renew_finalize)?, + DlcMessageType::Channel(DlcMessageSubType::RenewFinalize), + ), + ChannelMessage::RenewRevoke(renew_revoke) => ( + serde_json::to_string(&renew_revoke)?, + DlcMessageType::Channel(DlcMessageSubType::RenewRevoke), + ), + ChannelMessage::CollaborativeCloseOffer(collaborative_close_offer) => ( + serde_json::to_string(&collaborative_close_offer)?, + DlcMessageType::Channel(DlcMessageSubType::CollaborativeCloseOffer), + ), + ChannelMessage::Reject(reject) => ( + serde_json::to_string(&reject)?, + DlcMessageType::Channel(DlcMessageSubType::Reject), + ), + }, + _ => unreachable!(), + }; + + Ok(Self { + message, + message_type, + }) + } +} diff --git a/crates/ln-dlc-node/src/lib.rs b/crates/ln-dlc-node/src/lib.rs index 944ab44d6..546155993 100644 --- a/crates/ln-dlc-node/src/lib.rs +++ b/crates/ln-dlc-node/src/lib.rs @@ -34,6 +34,7 @@ mod shadow; pub mod channel; pub mod config; +pub mod dlc_message; pub mod ln; pub mod node; pub mod scorer; diff --git a/crates/ln-dlc-node/src/node/dlc_channel.rs b/crates/ln-dlc-node/src/node/dlc_channel.rs index 5f89bdf1d..d5b059802 100644 --- a/crates/ln-dlc-node/src/node/dlc_channel.rs +++ b/crates/ln-dlc-node/src/node/dlc_channel.rs @@ -1,3 +1,4 @@ +use crate::node::event::NodeEvent; use crate::node::Node; use crate::node::Storage as LnDlcStorage; use crate::storage::TenTenOneStorage; @@ -57,8 +58,7 @@ impl Nod let sub_channel_manager = self.sub_channel_manager.clone(); let oracles = contract_input.contract_infos[0].oracles.clone(); let event_id = oracles.event_id; - let dlc_message_handler = self.dlc_message_handler.clone(); - let peer_manager = self.peer_manager.clone(); + let event_handler = self.event_handler.clone(); move || { let announcements: Vec<_> = p2pd_oracles .into_iter() @@ -76,12 +76,13 @@ impl Nod .offer_channel(&contract_input, counterparty)?; let temporary_contract_id = sub_channel_offer.temporary_contract_id; - send_dlc_message( - &dlc_message_handler, - &peer_manager, - counterparty, - Message::Channel(ChannelMessage::Offer(sub_channel_offer)), - ); + + if let Err(e) = event_handler.publish(NodeEvent::SendDlcMessage { + peer: counterparty, + msg: Message::Channel(ChannelMessage::Offer(sub_channel_offer)), + }) { + tracing::error!("Failed to publish send dlc message node event! {e:#}"); + } Ok(temporary_contract_id) } @@ -100,19 +101,17 @@ impl Nod tracing::info!(channel_id = %hex::encode(dlc_channel_id), "Proposing a DLC channel update"); spawn_blocking({ let dlc_manager = self.dlc_manager.clone(); - let dlc_message_handler = self.dlc_message_handler.clone(); - let peer_manager = self.peer_manager.clone(); let dlc_channel_id = *dlc_channel_id; + let event_handler = self.event_handler.clone(); move || { let (renew_offer, counterparty_pubkey) = dlc_manager.renew_offer(&dlc_channel_id, payout_amount, &contract_input)?; - send_dlc_message( - &dlc_message_handler, - &peer_manager, - counterparty_pubkey, - Message::Channel(ChannelMessage::RenewOffer(renew_offer)), - ); + event_handler.publish(NodeEvent::SendDlcMessage { + peer: counterparty_pubkey, + msg: Message::Channel(ChannelMessage::RenewOffer(renew_offer)), + })?; + Ok(()) } }) @@ -148,12 +147,10 @@ impl Nod let (msg, _channel_id, _contract_id, counter_party) = self.dlc_manager.accept_channel(channel_id)?; - send_dlc_message( - &self.dlc_message_handler, - &self.peer_manager, - counter_party, - Message::Channel(ChannelMessage::Accept(msg)), - ); + self.event_handler.publish(NodeEvent::SendDlcMessage { + peer: counter_party, + msg: Message::Channel(ChannelMessage::Accept(msg)), + })?; Ok(()) } @@ -203,8 +200,7 @@ impl Nod SignedChannelState::Settled { .. } | SignedChannelState::RenewFinalized { .. } => { spawn_blocking({ let dlc_manager = self.dlc_manager.clone(); - let dlc_message_handler = self.dlc_message_handler.clone(); - let peer_manager = self.peer_manager.clone(); + let event_handler = self.event_handler.clone(); move || { let settle_offer = dlc_manager .offer_collaborative_close( @@ -215,12 +211,12 @@ impl Nod "Could not propose to collaboratively close the dlc channel.", )?; - send_dlc_message( - &dlc_message_handler, - &peer_manager, - counterparty, - Message::Channel(ChannelMessage::CollaborativeCloseOffer(settle_offer)), - ); + event_handler.publish(NodeEvent::SendDlcMessage { + peer: counterparty, + msg: Message::Channel(ChannelMessage::CollaborativeCloseOffer( + settle_offer, + )), + })?; anyhow::Ok(()) } @@ -252,18 +248,15 @@ impl Nod spawn_blocking({ let dlc_manager = self.dlc_manager.clone(); - let dlc_message_handler = self.dlc_message_handler.clone(); - let peer_manager = self.peer_manager.clone(); + let event_handler = self.event_handler.clone(); move || { let (settle_offer, counterparty) = dlc_manager.settle_offer(&channel_id, accept_settlement_amount)?; - send_dlc_message( - &dlc_message_handler, - &peer_manager, - counterparty, - Message::Channel(ChannelMessage::SettleOffer(settle_offer)), - ); + event_handler.publish(NodeEvent::SendDlcMessage { + peer: counterparty, + msg: Message::Channel(ChannelMessage::SettleOffer(settle_offer)), + })?; Ok(()) } @@ -291,16 +284,12 @@ impl Nod tracing::info!(channel_id = %channel_id_hex, "Accepting DLC channel collaborative settlement"); let dlc_manager = self.dlc_manager.clone(); - let dlc_message_handler = self.dlc_message_handler.clone(); - let peer_manager = self.peer_manager.clone(); let (settle_offer, counterparty_pk) = dlc_manager.accept_settle_offer(&channel_id)?; - send_dlc_message( - &dlc_message_handler, - &peer_manager, - counterparty_pk, - Message::Channel(ChannelMessage::SettleAccept(settle_offer)), - ); + self.event_handler.publish(NodeEvent::SendDlcMessage { + peer: counterparty_pk, + msg: Message::Channel(ChannelMessage::SettleAccept(settle_offer)), + })?; Ok(()) } diff --git a/crates/ln-dlc-node/src/node/event.rs b/crates/ln-dlc-node/src/node/event.rs index 203e332c3..d64ee8030 100644 --- a/crates/ln-dlc-node/src/node/event.rs +++ b/crates/ln-dlc-node/src/node/event.rs @@ -8,6 +8,7 @@ use tokio::sync::broadcast::Receiver; #[derive(Clone, Debug)] pub enum NodeEvent { Connected { peer: PublicKey }, + SendDlcMessage { peer: PublicKey, msg: Message }, } #[derive(Clone)] diff --git a/crates/ln-dlc-node/src/tests/mod.rs b/crates/ln-dlc-node/src/tests/mod.rs index 03eefe375..f9ee07892 100644 --- a/crates/ln-dlc-node/src/tests/mod.rs +++ b/crates/ln-dlc-node/src/tests/mod.rs @@ -1,5 +1,7 @@ use crate::config::app_config; use crate::config::coordinator_config; +use crate::node::dlc_channel::send_dlc_message; +use crate::node::event::NodeEvent; use crate::node::event::NodeEventHandler; use crate::node::peer_manager::alias_as_bytes; use crate::node::GossipSourceConfig; @@ -205,6 +207,7 @@ impl Node { let storage = TenTenOneInMemoryStorage::new(); + let event_handler = Arc::new(NodeEventHandler::new()); let node = Node::new( ldk_config, scorer::in_memory_scorer, @@ -223,10 +226,36 @@ impl Node { WalletSettings::default(), vec![oracle.into()], XOnlyPublicKey::from_str(ORACLE_PUBKEY)?, - Arc::new(NodeEventHandler::new()), + event_handler.clone(), )?; let node = Arc::new(node); + tokio::spawn({ + let mut receiver = event_handler.subscribe(); + let node = node.clone(); + async move { + loop { + match receiver.recv().await { + Ok(NodeEvent::SendDlcMessage { peer, msg }) => { + send_dlc_message( + &node.dlc_message_handler, + &node.peer_manager, + peer, + msg, + ); + } + Ok(NodeEvent::Connected { .. }) => {} // ignored + Err(_) => { + tracing::error!( + "Failed to receive message from node even handler channel." + ); + break; + } + } + } + } + }); + let event_handler = event_handler_factory(node.clone(), ldk_event_sender); let running = node.start(event_handler, false)?; diff --git a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/down.sql b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/down.sql new file mode 100644 index 000000000..c5a84b55d --- /dev/null +++ b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TABLE "last_outbound_dlc_messages"; +DROP TABLE "dlc_messages"; diff --git a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql new file mode 100644 index 000000000..29d48d63e --- /dev/null +++ b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -0,0 +1,17 @@ +CREATE TABLE "dlc_messages" ( + -- We need to store the hash as TEXT as the BIGINT type overflows on some u64 values breaking the hash value. + message_hash TEXT PRIMARY KEY NOT NULL, + inbound BOOLEAN NOT NULL, + peer_id TEXT NOT NULL, + message_type TEXT NOT NULL, + message_sub_type TEXT NOT NULL, + timestamp BIGINT NOT NULL +); + +CREATE TABLE "last_outbound_dlc_messages" ( + peer_id TEXT PRIMARY KEY NOT NULL, + message_hash TEXT REFERENCES dlc_messages(message_hash) NOT NULL, + message TEXT NOT NULL, + timestamp BIGINT NOT NULL +); + diff --git a/mobile/native/src/db/custom_types.rs b/mobile/native/src/db/custom_types.rs index 040a543bc..5d711d7f4 100644 --- a/mobile/native/src/db/custom_types.rs +++ b/mobile/native/src/db/custom_types.rs @@ -1,3 +1,5 @@ +use crate::db::dlc_messages::MessageSubType; +use crate::db::dlc_messages::MessageType; use crate::db::models::ChannelState; use crate::db::models::ContractSymbol; use crate::db::models::Direction; @@ -265,6 +267,76 @@ impl FromSql for ChannelState { } } +impl ToSql for MessageType { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + let text = match *self { + MessageType::OnChain => "OnChain", + MessageType::Channel => "Channel", + }; + out.set_value(text); + Ok(IsNull::No) + } +} + +impl FromSql for MessageType { + fn from_sql(bytes: backend::RawValue) -> deserialize::Result { + let string = >::from_sql(bytes)?; + + return match string.as_str() { + "OnChain" => Ok(MessageType::OnChain), + "Channel" => Ok(MessageType::Channel), + _ => Err("Unrecognized enum variant".into()), + }; + } +} + +impl ToSql for MessageSubType { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + let text = match *self { + MessageSubType::Offer => "Offer", + MessageSubType::Accept => "Accept", + MessageSubType::Sign => "Sign", + MessageSubType::SettleOffer => "SettleOffer", + MessageSubType::SettleAccept => "SettleAccept", + MessageSubType::SettleConfirm => "SettleConfirm", + MessageSubType::SettleFinalize => "SettleFinalize", + MessageSubType::RenewOffer => "RenewOffer", + MessageSubType::RenewAccept => "RenewAccept", + MessageSubType::RenewConfirm => "RenewConfirm", + MessageSubType::RenewFinalize => "RenewFinalize", + MessageSubType::RenewRevoke => "RenewRevoke", + MessageSubType::CollaborativeCloseOffer => "CollaborativeCloseOffer", + MessageSubType::Reject => "Reject", + }; + out.set_value(text); + Ok(IsNull::No) + } +} + +impl FromSql for MessageSubType { + fn from_sql(bytes: backend::RawValue) -> deserialize::Result { + let string = >::from_sql(bytes)?; + + return match string.as_str() { + "Offer" => Ok(MessageSubType::Offer), + "Accept" => Ok(MessageSubType::Accept), + "Sign" => Ok(MessageSubType::Sign), + "SettleOffer" => Ok(MessageSubType::SettleOffer), + "SettleAccept" => Ok(MessageSubType::SettleAccept), + "SettleConfirm" => Ok(MessageSubType::SettleConfirm), + "SettleFinalize" => Ok(MessageSubType::SettleFinalize), + "RenewOffer" => Ok(MessageSubType::RenewOffer), + "RenewAccept" => Ok(MessageSubType::RenewAccept), + "RenewConfirm" => Ok(MessageSubType::RenewConfirm), + "RenewFinalize" => Ok(MessageSubType::RenewFinalize), + "RenewRevoke" => Ok(MessageSubType::RenewRevoke), + "CollaborativeCloseOffer" => Ok(MessageSubType::CollaborativeCloseOffer), + "Reject" => Ok(MessageSubType::Reject), + _ => Err("Unrecognized enum variant".into()), + }; + } +} + #[cfg(test)] mod tests { use crate::db::custom_types::tests::customstruct::id; diff --git a/mobile/native/src/db/dlc_messages.rs b/mobile/native/src/db/dlc_messages.rs new file mode 100644 index 000000000..ae5e26ceb --- /dev/null +++ b/mobile/native/src/db/dlc_messages.rs @@ -0,0 +1,195 @@ +use crate::schema; +use anyhow::ensure; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::prelude::*; +use diesel::sql_types::Text; +use diesel::AsChangeset; +use diesel::AsExpression; +use diesel::FromSqlRow; +use diesel::Insertable; +use diesel::OptionalExtension; +use diesel::QueryResult; +use diesel::Queryable; +use diesel::QueryableByName; +use diesel::RunQueryDsl; +use diesel::SqliteConnection; +use schema::dlc_messages; +use std::str::FromStr; +use time::OffsetDateTime; + +#[derive(Insertable, QueryableByName, Queryable, Debug, Clone, PartialEq, AsChangeset)] +#[diesel(table_name = dlc_messages)] +pub(crate) struct DlcMessage { + pub message_hash: String, + pub inbound: bool, + pub peer_id: String, + pub message_type: MessageType, + pub message_sub_type: MessageSubType, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = Text)] +pub enum MessageType { + OnChain, + Channel, +} + +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = Text)] +pub enum MessageSubType { + Offer, + Accept, + Sign, + SettleOffer, + SettleAccept, + SettleConfirm, + SettleFinalize, + RenewOffer, + RenewAccept, + RenewConfirm, + RenewFinalize, + RenewRevoke, + CollaborativeCloseOffer, + Reject, +} + +impl DlcMessage { + pub(crate) fn get( + conn: &mut SqliteConnection, + message_hash: u64, + ) -> QueryResult> { + let result = schema::dlc_messages::table + .filter(schema::dlc_messages::message_hash.eq(message_hash.to_string())) + .first::(conn) + .optional()?; + + Ok(result.map(|q| q.into())) + } + + pub(crate) fn insert( + conn: &mut SqliteConnection, + dlc_message: ln_dlc_node::dlc_message::DlcMessage, + ) -> Result<()> { + let affected_rows = diesel::insert_into(schema::dlc_messages::table) + .values(DlcMessage::from(dlc_message)) + .execute(conn)?; + + ensure!(affected_rows > 0, "Could not insert queue"); + + Ok(()) + } +} + +impl From for DlcMessage { + fn from(value: ln_dlc_node::dlc_message::DlcMessage) -> Self { + Self { + message_hash: value.message_hash.to_string(), + peer_id: value.peer_id.to_string(), + message_type: MessageType::from(value.clone().message_type), + message_sub_type: MessageSubType::from(value.message_type), + timestamp: value.timestamp.unix_timestamp(), + inbound: value.inbound, + } + } +} + +impl From for MessageType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { + match value { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(_) => Self::OnChain, + ln_dlc_node::dlc_message::DlcMessageType::Channel(_) => Self::Channel, + } + } +} + +impl From for MessageSubType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { + let message_sub_type = match value { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(message_sub_type) => message_sub_type, + ln_dlc_node::dlc_message::DlcMessageType::Channel(message_sub_type) => message_sub_type, + }; + MessageSubType::from(message_sub_type) + } +} + +impl From for MessageSubType { + fn from(value: ln_dlc_node::dlc_message::DlcMessageSubType) -> Self { + match value { + ln_dlc_node::dlc_message::DlcMessageSubType::Offer => Self::Offer, + ln_dlc_node::dlc_message::DlcMessageSubType::Accept => Self::Accept, + ln_dlc_node::dlc_message::DlcMessageSubType::Sign => Self::Sign, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer => Self::SettleOffer, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept => Self::SettleAccept, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm => Self::SettleConfirm, + ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize => Self::SettleFinalize, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer => Self::RenewOffer, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept => Self::RenewAccept, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm => Self::RenewConfirm, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize => Self::RenewFinalize, + ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke => Self::RenewRevoke, + ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer => { + Self::CollaborativeCloseOffer + } + ln_dlc_node::dlc_message::DlcMessageSubType::Reject => Self::Reject, + } + } +} + +impl From for ln_dlc_node::dlc_message::DlcMessage { + fn from(value: DlcMessage) -> Self { + let dlc_message_sub_type = + ln_dlc_node::dlc_message::DlcMessageSubType::from(value.clone().message_sub_type); + let dlc_message_type = match &value.message_type { + MessageType::OnChain => { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) + } + MessageType::Channel => { + ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) + } + }; + + Self { + message_hash: u64::from_str(&value.message_hash).expect("valid u64"), + inbound: value.inbound, + message_type: dlc_message_type, + peer_id: PublicKey::from_str(&value.peer_id).expect("valid public key"), + timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp) + .expect("valid timestamp"), + } + } +} + +impl From for ln_dlc_node::dlc_message::DlcMessageSubType { + fn from(value: MessageSubType) -> Self { + match value { + MessageSubType::Offer => ln_dlc_node::dlc_message::DlcMessageSubType::Offer, + MessageSubType::Accept => ln_dlc_node::dlc_message::DlcMessageSubType::Accept, + MessageSubType::Sign => ln_dlc_node::dlc_message::DlcMessageSubType::Sign, + MessageSubType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer, + MessageSubType::SettleAccept => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept + } + MessageSubType::SettleConfirm => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm + } + MessageSubType::SettleFinalize => { + ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize + } + MessageSubType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer, + MessageSubType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept, + MessageSubType::RenewConfirm => { + ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm + } + MessageSubType::RenewFinalize => { + ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize + } + MessageSubType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke, + MessageSubType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer + } + MessageSubType::Reject => ln_dlc_node::dlc_message::DlcMessageSubType::Reject, + } + } +} diff --git a/mobile/native/src/db/last_outbound_dlc_messages.rs b/mobile/native/src/db/last_outbound_dlc_messages.rs new file mode 100644 index 000000000..c6b581616 --- /dev/null +++ b/mobile/native/src/db/last_outbound_dlc_messages.rs @@ -0,0 +1,99 @@ +use crate::db::dlc_messages::MessageSubType; +use crate::db::dlc_messages::MessageType; +use crate::schema; +use crate::schema::dlc_messages; +use crate::schema::last_outbound_dlc_messages; +use anyhow::ensure; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::AsChangeset; +use diesel::ExpressionMethods; +use diesel::Insertable; +use diesel::JoinOnDsl; +use diesel::OptionalExtension; +use diesel::QueryDsl; +use diesel::QueryResult; +use diesel::Queryable; +use diesel::RunQueryDsl; +use diesel::SqliteConnection; +use ln_dlc_node::dlc_message::SerializedDlcMessage; +use time::OffsetDateTime; + +#[derive(Insertable, Queryable, Debug, Clone, PartialEq, AsChangeset)] +#[diesel(table_name = last_outbound_dlc_messages)] +pub(crate) struct LastOutboundDlcMessage { + pub peer_id: String, + pub message_hash: String, + pub message: String, + pub timestamp: i64, +} + +impl LastOutboundDlcMessage { + pub(crate) fn get( + conn: &mut SqliteConnection, + peer_id: &PublicKey, + ) -> QueryResult> { + let last_outbound_dlc_message = last_outbound_dlc_messages::table + .inner_join( + dlc_messages::table + .on(dlc_messages::message_hash.eq(last_outbound_dlc_messages::message_hash)), + ) + .filter(last_outbound_dlc_messages::peer_id.eq(peer_id.to_string())) + .select(( + dlc_messages::message_type, + dlc_messages::message_sub_type, + last_outbound_dlc_messages::message, + )) + .first::<(MessageType, MessageSubType, String)>(conn) + .optional()?; + + let serialized_dlc_message = match last_outbound_dlc_message { + Some((message_type, message_sub_type, message)) => { + let dlc_message_sub_type = + ln_dlc_node::dlc_message::DlcMessageSubType::from(message_sub_type); + let message_type = match &message_type { + MessageType::OnChain => { + ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) + } + MessageType::Channel => { + ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) + } + }; + + Some(SerializedDlcMessage { + message, + message_type, + }) + } + None => None, + }; + + Ok(serialized_dlc_message) + } + + pub(crate) fn upsert( + conn: &mut SqliteConnection, + peer_id: &PublicKey, + sdm: SerializedDlcMessage, + ) -> Result<()> { + let values = ( + last_outbound_dlc_messages::peer_id.eq(peer_id.to_string()), + last_outbound_dlc_messages::message_hash.eq(sdm.generate_hash().to_string()), + last_outbound_dlc_messages::message.eq(sdm.message), + last_outbound_dlc_messages::timestamp.eq(OffsetDateTime::now_utc().unix_timestamp()), + ); + let affected_rows = diesel::insert_into(last_outbound_dlc_messages::table) + .values(&values.clone()) + .on_conflict(schema::last_outbound_dlc_messages::peer_id) + .do_update() + .set(values) + .execute(conn)?; + + ensure!( + affected_rows > 0, + "Could not upsert last outbound dlc messages" + ); + + Ok(()) + } +} diff --git a/mobile/native/src/db/mod.rs b/mobile/native/src/db/mod.rs index 4eea7921d..c42bd5ca4 100644 --- a/mobile/native/src/db/mod.rs +++ b/mobile/native/src/db/mod.rs @@ -42,6 +42,8 @@ use time::OffsetDateTime; use uuid::Uuid; mod custom_types; +pub mod dlc_messages; +pub mod last_outbound_dlc_messages; pub mod models; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); diff --git a/mobile/native/src/dlc_handler.rs b/mobile/native/src/dlc_handler.rs new file mode 100644 index 000000000..a74a76593 --- /dev/null +++ b/mobile/native/src/dlc_handler.rs @@ -0,0 +1,158 @@ +use crate::db; +use crate::ln_dlc::node::NodeStorage; +use crate::storage::TenTenOneNodeStorage; +use anyhow::Result; +use bitcoin::secp256k1::PublicKey; +use diesel::SqliteConnection; +use dlc_messages::Message; +use ln_dlc_node::dlc_message::DlcMessage; +use ln_dlc_node::dlc_message::SerializedDlcMessage; +use ln_dlc_node::node::dlc_channel::send_dlc_message; +use ln_dlc_node::node::event::NodeEvent; +use ln_dlc_node::node::Node; +use std::sync::Arc; +use tokio::sync::broadcast; +use tokio::sync::broadcast::error::RecvError; + +/// The DlcHandler is responsible for sending dlc messages and marking received ones as +/// processed. It's main purpose is to ensure the following. +/// +/// 1. Mark all received inbound messages as processed. +/// 2. Save the last outbound dlc message, so it can be resend on the next reconnect. +/// 3. Check if a receive message has already been processed and if so inform to skip the message. + +#[derive(Clone)] +pub struct DlcHandler { + node: Arc>, +} + +impl DlcHandler { + pub fn new(node: Arc>) -> Self { + DlcHandler { node } + } +} +pub async fn handle_dlc_messages( + dlc_handler: DlcHandler, + mut receiver: broadcast::Receiver, +) { + loop { + match receiver.recv().await { + Ok(NodeEvent::Connected { peer }) => { + if let Err(e) = dlc_handler.on_connect(peer) { + tracing::error!(peer=%peer, "Failed to process on connect event. {e:#}"); + } + } + Ok(NodeEvent::SendDlcMessage { peer, msg }) => { + if let Err(e) = dlc_handler.send_dlc_message(peer, msg) { + tracing::error!(peer=%peer, "Failed to process end dlc message event. {e:#}"); + } + } + Err(RecvError::Lagged(skipped)) => { + tracing::warn!("Skipped {skipped} messages"); + } + Err(RecvError::Closed) => { + tracing::error!("Lost connection to sender!"); + break; + } + } + } +} + +impl DlcHandler { + pub fn send_dlc_message(&self, peer: PublicKey, msg: Message) -> Result<()> { + let mut conn = db::connection()?; + + let serialized_outbound_message = SerializedDlcMessage::try_from(&msg)?; + let outbound_msg = DlcMessage::new(peer, serialized_outbound_message.clone(), false)?; + + db::dlc_messages::DlcMessage::insert(&mut conn, outbound_msg)?; + db::last_outbound_dlc_messages::LastOutboundDlcMessage::upsert( + &mut conn, + &peer, + serialized_outbound_message, + )?; + + send_dlc_message( + &self.node.dlc_message_handler, + &self.node.peer_manager, + peer, + msg, + ); + + Ok(()) + } + + pub fn on_connect(&self, peer: PublicKey) -> Result<()> { + let mut conn = db::connection()?; + let last_outbound_serialized_dlc_message = + db::last_outbound_dlc_messages::LastOutboundDlcMessage::get(&mut conn, &peer)?; + + if let Some(last_outbound_serialized_dlc_message) = last_outbound_serialized_dlc_message { + tracing::debug!(%peer, ?last_outbound_serialized_dlc_message.message_type, "Sending last dlc message"); + + let message = Message::try_from(&last_outbound_serialized_dlc_message)?; + send_dlc_message( + &self.node.dlc_message_handler, + &self.node.peer_manager, + peer, + message, + ); + } else { + tracing::debug!(%peer, "No last dlc message found. Nothing todo."); + } + + Ok(()) + } + // Returns either the dlc message step or return none, if the dlc message has already been + // processed. + pub fn start_dlc_message_step( + conn: &mut SqliteConnection, + msg: &Message, + peer_id: PublicKey, + ) -> Result> { + let serialized_inbound_message = SerializedDlcMessage::try_from(msg)?; + let inbound_msg = DlcMessage::new(peer_id, serialized_inbound_message, true)?; + + let dlc_message_step = + match db::dlc_messages::DlcMessage::get(conn, inbound_msg.message_hash)? { + Some(_) => None, // the dlc message has already been processed, no step necessary. + None => Some(DlcMessageStep { + inbound_msg, + peer_id, + }), + }; + + Ok(dlc_message_step) + } +} + +pub struct DlcMessageStep { + pub peer_id: PublicKey, + pub inbound_msg: DlcMessage, +} + +impl DlcMessageStep { + /// Finishes the current dlc step by storing the received inbound message as processed and + /// caching the last outbound dlc message (if any) into the database. + pub fn finish(&self, conn: &mut SqliteConnection, response: &Option) -> Result<()> { + tracing::debug!("Marking the received message as processed"); + + db::dlc_messages::DlcMessage::insert(conn, self.inbound_msg.clone())?; + + if let Some(resp) = response { + tracing::debug!("Persisting last outbound dlc message"); + let serialized_outbound_message = SerializedDlcMessage::try_from(resp)?; + let outbound_msg = + DlcMessage::new(self.peer_id, serialized_outbound_message.clone(), false)?; + + db::dlc_messages::DlcMessage::insert(conn, outbound_msg)?; + db::last_outbound_dlc_messages::LastOutboundDlcMessage::upsert( + conn, + &self.peer_id, + serialized_outbound_message, + )?; + } + + Ok(()) + } +} diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs index 86d0035f5..fedda3345 100644 --- a/mobile/native/src/lib.rs +++ b/mobile/native/src/lib.rs @@ -26,4 +26,5 @@ mod bridge_generated; mod channel_trade_constraints; mod cipher; mod destination; +mod dlc_handler; mod storage; diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 2371ae296..1e04d2c32 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -10,6 +10,7 @@ use crate::commons::reqwest_client; use crate::config; use crate::config::get_rgs_server_url; use crate::db; +use crate::dlc_handler; use crate::event; use crate::event::EventInternal; use crate::ln_dlc::channel_status::track_channel_status; @@ -104,6 +105,7 @@ mod sync_position_to_subchannel; pub mod channel_status; +use crate::dlc_handler::DlcHandler; use crate::storage::TenTenOneNodeStorage; pub use channel_status::ChannelStatus; use ln_dlc_node::node::event::NodeEventHandler; @@ -322,6 +324,16 @@ pub fn run(seed_dir: String, runtime: &Runtime) -> Result<()> { )?; let node = Arc::new(node); + let dlc_handler = DlcHandler::new(node.clone()); + runtime.spawn(async move { + // this handles sending outbound dlc messages as well as keeping track of what + // dlc messages have already been processed and what was the last outbound dlc message + // so it can be resend on reconnect. + // + // this does not handle the incoming dlc messages! + dlc_handler::handle_dlc_messages(dlc_handler, node_event_handler.subscribe()).await + }); + let event_handler = AppEventHandler::new(node.clone(), Some(event_sender)); let _running = node.start(event_handler, true)?; let node = Arc::new(Node::new(node, _running)); diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index d86e4082f..fe1efecf0 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -1,4 +1,5 @@ use crate::db; +use crate::dlc_handler::DlcHandler; use crate::event; use crate::event::BackgroundTask; use crate::event::EventInternal; @@ -150,96 +151,139 @@ impl Node { ); let resp = match &msg { - Message::OnChain(_) => self - .inner - .dlc_manager - .on_dlc_message(&msg, node_id) - .with_context(|| { - format!( - "Failed to handle {} message from {node_id}", - dlc_message_name(&msg) - ) - })?, - Message::SubChannel(ref msg) => { + Message::OnChain(_) => { + let dlc_message_step = { + let mut conn = db::connection()?; + let dlc_message_step = + DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; + + match dlc_message_step { + Some(dlc_message_step) => dlc_message_step, + None => { + tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); + return Ok(()); + } + } + }; + let resp = self .inner - .sub_channel_manager - .on_sub_channel_message(msg, &node_id) + .dlc_manager + .on_dlc_message(&msg, node_id) .with_context(|| { format!( "Failed to handle {} message from {node_id}", - sub_channel_message_name(msg) + dlc_message_name(&msg) ) - })? - .map(Message::SubChannel); + })?; - // Some incoming messages require extra action from our part for the protocol to - // continue - match msg { - SubChannelMessage::Offer(offer) => { - // TODO: We should probably verify that: (1) the counterparty is the - // coordinator and (2) the DLC channel offer is expected and correct. + { + let mut conn = db::connection()?; + dlc_message_step.finish(&mut conn, &resp)?; + } + + resp + } + Message::Channel(channel_msg) => { + let dlc_message_step = { + let mut conn = db::connection()?; + let dlc_message_step = + DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; + + match dlc_message_step { + Some(dlc_message_step) => dlc_message_step, + None => { + tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); + return Ok(()); + } + } + }; + + let resp = self + .inner + .dlc_manager + .on_dlc_message(&msg, node_id) + .with_context(|| { + format!( + "Failed to handle {} message from {node_id}", + dlc_message_name(&msg) + ) + })?; + + match channel_msg { + ChannelMessage::Offer(offer) => { let action = decide_subchannel_offer_action( OffsetDateTime::from_unix_timestamp( offer.contract_info.get_closest_maturity_date() as i64, ) .expect("A contract should always have a valid maturity timestamp."), ); - self.process_subchannel_offer(offer.channel_id, action)?; + self.process_dlc_channel_offer(offer.temporary_channel_id, action)?; } - SubChannelMessage::CloseOffer(offer) => { - let channel_id = offer.channel_id; - - // TODO: We should probably verify that: (1) the counterparty is the - // coordinator and (2) the DLC channel close offer is expected and correct. + ChannelMessage::SettleOffer(offer) => { self.inner - .accept_sub_channel_collaborative_settlement(&channel_id) + .accept_dlc_channel_collaborative_settlement(offer.channel_id) .with_context(|| { format!( - "Failed to accept sub channel close offer for channel {}", - hex::encode(channel_id.0) + "Failed to accept DLC channel close offer for channel {}", + hex::encode(offer.channel_id) ) })?; } _ => (), - }; + } + + { + let mut conn = db::connection()?; + dlc_message_step.finish(&mut conn, &resp)?; + } resp } - Message::Channel(channel_msg) => { + Message::SubChannel(ref msg) => { let resp = self .inner - .dlc_manager - .on_dlc_message(&msg, node_id) + .sub_channel_manager + .on_sub_channel_message(msg, &node_id) .with_context(|| { format!( "Failed to handle {} message from {node_id}", - dlc_message_name(&msg) + sub_channel_message_name(msg) ) - })?; + })? + .map(Message::SubChannel); - match channel_msg { - ChannelMessage::Offer(offer) => { + // Some incoming messages require extra action from our part for the protocol to + // continue + match msg { + SubChannelMessage::Offer(offer) => { + // TODO: We should probably verify that: (1) the counterparty is the + // coordinator and (2) the DLC channel offer is expected and correct. let action = decide_subchannel_offer_action( OffsetDateTime::from_unix_timestamp( offer.contract_info.get_closest_maturity_date() as i64, ) .expect("A contract should always have a valid maturity timestamp."), ); - self.process_dlc_channel_offer(offer.temporary_channel_id, action)?; + self.process_subchannel_offer(offer.channel_id, action)?; } - ChannelMessage::SettleOffer(offer) => { + SubChannelMessage::CloseOffer(offer) => { + let channel_id = offer.channel_id; + + // TODO: We should probably verify that: (1) the counterparty is the + // coordinator and (2) the DLC channel close offer is expected and correct. self.inner - .accept_dlc_channel_collaborative_settlement(offer.channel_id) + .accept_sub_channel_collaborative_settlement(&channel_id) .with_context(|| { format!( - "Failed to accept DLC channel close offer for channel {}", - hex::encode(offer.channel_id) + "Failed to accept sub channel close offer for channel {}", + hex::encode(channel_id.0) ) })?; } _ => (), - } + }; + resp } }; diff --git a/mobile/native/src/schema.rs b/mobile/native/src/schema.rs index 81448021d..ce6145bee 100644 --- a/mobile/native/src/schema.rs +++ b/mobile/native/src/schema.rs @@ -17,6 +17,26 @@ diesel::table! { } } +diesel::table! { + dlc_messages (message_hash) { + message_hash -> Text, + inbound -> Bool, + peer_id -> Text, + message_type -> Text, + message_sub_type -> Text, + timestamp -> BigInt, + } +} + +diesel::table! { + last_outbound_dlc_messages (peer_id) { + peer_id -> Text, + message_hash -> Text, + message -> Text, + timestamp -> BigInt, + } +} + diesel::table! { orders (id) { id -> Text, @@ -104,8 +124,12 @@ diesel::table! { } } +diesel::joinable!(last_outbound_dlc_messages -> dlc_messages (message_hash)); + diesel::allow_tables_to_appear_in_same_query!( channels, + dlc_messages, + last_outbound_dlc_messages, orders, payments, positions, From 7bdbe5974a6cff9bfbc2416ffbe93aa5830ac7e6 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Mon, 15 Jan 2024 14:14:05 +0100 Subject: [PATCH 6/8] chore: Fix compile error in test subscribers After e268c62823eec105d2d6e0810cd29dec181e7255, the order can't be copied anymore. --- crates/ln-dlc-node/src/node/dlc_manager.rs | 2 +- crates/tests-e2e/src/test_subscriber.rs | 4 ++-- mobile/native/src/ln_dlc/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ln-dlc-node/src/node/dlc_manager.rs b/crates/ln-dlc-node/src/node/dlc_manager.rs index cf674356c..c0737e5e4 100644 --- a/crates/ln-dlc-node/src/node/dlc_manager.rs +++ b/crates/ln-dlc-node/src/node/dlc_manager.rs @@ -83,7 +83,7 @@ pub fn signed_channel_state_name(signed_channel: &SignedChannel) -> String { impl Node { pub fn get_signed_channel_by_trader_id(&self, trader_id: PublicKey) -> Result { - let dlc_channels = self.list_dlc_channels()?; + let dlc_channels = self.list_signed_dlc_channels()?; let signed_channel = dlc_channels .iter() .find(|channel| channel.counter_party == trader_id) diff --git a/crates/tests-e2e/src/test_subscriber.rs b/crates/tests-e2e/src/test_subscriber.rs index 5aec4b760..0904eaf67 100644 --- a/crates/tests-e2e/src/test_subscriber.rs +++ b/crates/tests-e2e/src/test_subscriber.rs @@ -102,7 +102,7 @@ impl TestSubscriber { } pub fn order(&self) -> Option { - self.order.borrow().as_ref().copied() + self.order.borrow().as_ref().cloned() } pub fn order_filled(&self) -> Option> { @@ -170,7 +170,7 @@ impl Senders { // Ignore log events for now } native::event::EventInternal::OrderUpdateNotification(order) => { - self.order.send(Some(*order))?; + self.order.send(Some(order.clone()))?; } native::event::EventInternal::WalletInfoUpdateNotification(wallet_info) => { self.wallet_info.send(Some(wallet_info.clone()))?; diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index 1e04d2c32..aeb6cb025 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -1258,7 +1258,7 @@ pub async fn trade(trade_params: TradeParams) -> Result<(), (FailureReason, Erro pub async fn rollover(contract_id: Option) -> Result<()> { let node = state::get_node(); - let dlc_channels = node.inner.list_dlc_channels()?; + let dlc_channels = node.inner.list_signed_dlc_channels()?; let dlc_channel = dlc_channels .into_iter() From f316aea59af974bb21d0b757802bc03b60e2c724 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 16 Jan 2024 08:09:32 +0100 Subject: [PATCH 7/8] chore: Cleanup dlc message handling The node was still using its own implementation for sending dlc message, this was primarily needed because it also had to support `SubChannelMessages`. This change removes all subchannel and onchain protocol handlings from processing incoming dlc messages, as those protocols aren't used. By switching to the common `NodeEvent::SendDlcMessage` we can rely on the outbound message being persisted during sending and can get rid of the `DlcMessageStep`. --- .../down.sql | 1 - .../2024-01-10-105231_add_dlc_messages/up.sql | 6 - coordinator/src/db/custom_types.rs | 78 ++-- coordinator/src/db/dlc_messages.rs | 129 ++----- .../src/db/last_outbound_dlc_message.rs | 29 +- coordinator/src/dlc_handler.rs | 52 --- coordinator/src/node.rs | 228 +++-------- coordinator/src/schema.rs | 6 - crates/ln-dlc-node/src/dlc_message.rs | 181 +++------ .../2024-01-10-105231_add_dlc_messages/up.sql | 1 - mobile/native/src/db/custom_types.rs | 80 ++-- mobile/native/src/db/dlc_messages.rs | 114 ++---- .../src/db/last_outbound_dlc_messages.rs | 29 +- mobile/native/src/dlc_handler.rs | 53 --- mobile/native/src/ln_dlc/node.rs | 361 +++++------------- mobile/native/src/schema.rs | 1 - 16 files changed, 364 insertions(+), 985 deletions(-) diff --git a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql index c5f672274..7380f3ea8 100644 --- a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql +++ b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/down.sql @@ -2,6 +2,5 @@ DROP TABLE "last_outbound_dlc_messages"; DROP TABLE "dlc_messages"; -DROP TYPE "Message_Sub_Type_Type"; DROP TYPE "Message_Type_Type"; diff --git a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql index 4d52890e7..832f0dc57 100644 --- a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql +++ b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -1,9 +1,4 @@ CREATE TYPE "Message_Type_Type" AS ENUM ( - 'OnChain', - 'Channel' -); - -CREATE TYPE "Message_Sub_Type_Type" AS ENUM ( 'Offer', 'Accept', 'Sign', @@ -26,7 +21,6 @@ CREATE TABLE "dlc_messages" ( inbound BOOLEAN NOT NULL, peer_id TEXT NOT NULL, message_type "Message_Type_Type" NOT NULL, - message_sub_type "Message_Sub_Type_Type" NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/coordinator/src/db/custom_types.rs b/coordinator/src/db/custom_types.rs index 88c34bfdf..a943b5f17 100644 --- a/coordinator/src/db/custom_types.rs +++ b/coordinator/src/db/custom_types.rs @@ -1,5 +1,4 @@ use crate::db::channels::ChannelState; -use crate::db::dlc_messages::MessageSubType; use crate::db::dlc_messages::MessageType; use crate::db::payments::HtlcStatus; use crate::db::payments::PaymentFlow; @@ -9,7 +8,6 @@ use crate::schema::sql_types::ChannelStateType; use crate::schema::sql_types::ContractSymbolType; use crate::schema::sql_types::DirectionType; use crate::schema::sql_types::HtlcStatusType; -use crate::schema::sql_types::MessageSubTypeType; use crate::schema::sql_types::MessageTypeType; use crate::schema::sql_types::PaymentFlowType; use crate::schema::sql_types::PositionStateType; @@ -167,8 +165,20 @@ impl FromSql for Direction { impl ToSql for MessageType { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { match *self { - MessageType::OnChain => out.write_all(b"OnChain")?, - MessageType::Channel => out.write_all(b"Channel")?, + MessageType::Offer => out.write_all(b"Offer")?, + MessageType::Accept => out.write_all(b"Accept")?, + MessageType::Sign => out.write_all(b"Sign")?, + MessageType::SettleOffer => out.write_all(b"SettleOffer")?, + MessageType::SettleAccept => out.write_all(b"SettleAccept")?, + MessageType::SettleConfirm => out.write_all(b"SettleConfirm")?, + MessageType::SettleFinalize => out.write_all(b"SettleFinalize")?, + MessageType::RenewOffer => out.write_all(b"RenewOffer")?, + MessageType::RenewAccept => out.write_all(b"RenewAccept")?, + MessageType::RenewConfirm => out.write_all(b"RenewConfirm")?, + MessageType::RenewFinalize => out.write_all(b"RenewFinalize")?, + MessageType::RenewRevoke => out.write_all(b"RenewRevoke")?, + MessageType::CollaborativeCloseOffer => out.write_all(b"CollaborativeCloseOffer")?, + MessageType::Reject => out.write_all(b"Reject")?, } Ok(IsNull::No) } @@ -177,52 +187,20 @@ impl ToSql for MessageType { impl FromSql for MessageType { fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { match bytes.as_bytes() { - b"OnChain" => Ok(MessageType::OnChain), - b"Channel" => Ok(MessageType::Channel), - _ => Err("Unrecognized enum variant".into()), - } - } -} - -impl ToSql for MessageSubType { - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { - match *self { - MessageSubType::Offer => out.write_all(b"Offer")?, - MessageSubType::Accept => out.write_all(b"Accept")?, - MessageSubType::Sign => out.write_all(b"Sign")?, - MessageSubType::SettleOffer => out.write_all(b"SettleOffer")?, - MessageSubType::SettleAccept => out.write_all(b"SettleAccept")?, - MessageSubType::SettleConfirm => out.write_all(b"SettleConfirm")?, - MessageSubType::SettleFinalize => out.write_all(b"SettleFinalize")?, - MessageSubType::RenewOffer => out.write_all(b"RenewOffer")?, - MessageSubType::RenewAccept => out.write_all(b"RenewAccept")?, - MessageSubType::RenewConfirm => out.write_all(b"RenewConfirm")?, - MessageSubType::RenewFinalize => out.write_all(b"RenewFinalize")?, - MessageSubType::RenewRevoke => out.write_all(b"RenewRevoke")?, - MessageSubType::CollaborativeCloseOffer => out.write_all(b"CollaborativeCloseOffer")?, - MessageSubType::Reject => out.write_all(b"Reject")?, - } - Ok(IsNull::No) - } -} - -impl FromSql for MessageSubType { - fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { - match bytes.as_bytes() { - b"Offer" => Ok(MessageSubType::Offer), - b"Accept" => Ok(MessageSubType::Accept), - b"Sign" => Ok(MessageSubType::Sign), - b"SettleOffer" => Ok(MessageSubType::SettleOffer), - b"SettleAccept" => Ok(MessageSubType::SettleAccept), - b"SettleConfirm" => Ok(MessageSubType::SettleConfirm), - b"SettleFinalize" => Ok(MessageSubType::SettleFinalize), - b"RenewOffer" => Ok(MessageSubType::RenewOffer), - b"RenewAccept" => Ok(MessageSubType::RenewAccept), - b"RenewConfirm" => Ok(MessageSubType::RenewConfirm), - b"RenewFinalize" => Ok(MessageSubType::RenewFinalize), - b"RenewRevoke" => Ok(MessageSubType::RenewRevoke), - b"CollaborativeCloseOffer" => Ok(MessageSubType::CollaborativeCloseOffer), - b"Reject" => Ok(MessageSubType::Reject), + b"Offer" => Ok(MessageType::Offer), + b"Accept" => Ok(MessageType::Accept), + b"Sign" => Ok(MessageType::Sign), + b"SettleOffer" => Ok(MessageType::SettleOffer), + b"SettleAccept" => Ok(MessageType::SettleAccept), + b"SettleConfirm" => Ok(MessageType::SettleConfirm), + b"SettleFinalize" => Ok(MessageType::SettleFinalize), + b"RenewOffer" => Ok(MessageType::RenewOffer), + b"RenewAccept" => Ok(MessageType::RenewAccept), + b"RenewConfirm" => Ok(MessageType::RenewConfirm), + b"RenewFinalize" => Ok(MessageType::RenewFinalize), + b"RenewRevoke" => Ok(MessageType::RenewRevoke), + b"CollaborativeCloseOffer" => Ok(MessageType::CollaborativeCloseOffer), + b"Reject" => Ok(MessageType::Reject), _ => Err("Unrecognized enum variant".into()), } } diff --git a/coordinator/src/db/dlc_messages.rs b/coordinator/src/db/dlc_messages.rs index 9250aab60..4e4a94425 100644 --- a/coordinator/src/db/dlc_messages.rs +++ b/coordinator/src/db/dlc_messages.rs @@ -1,6 +1,5 @@ use crate::schema; use crate::schema::dlc_messages; -use crate::schema::sql_types::MessageSubTypeType; use crate::schema::sql_types::MessageTypeType; use anyhow::ensure; use anyhow::Result; @@ -25,22 +24,6 @@ use time::OffsetDateTime; #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] #[diesel(sql_type = MessageTypeType)] pub(crate) enum MessageType { - OnChain, - Channel, -} - -impl QueryId for MessageTypeType { - type QueryId = MessageTypeType; - const HAS_STATIC_QUERY_ID: bool = false; - - fn query_id() -> Option { - None - } -} - -#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] -#[diesel(sql_type = MessageSubTypeType)] -pub(crate) enum MessageSubType { Offer, Accept, Sign, @@ -57,8 +40,8 @@ pub(crate) enum MessageSubType { Reject, } -impl QueryId for MessageSubTypeType { - type QueryId = MessageSubTypeType; +impl QueryId for MessageTypeType { + type QueryId = MessageTypeType; const HAS_STATIC_QUERY_ID: bool = false; fn query_id() -> Option { @@ -73,7 +56,6 @@ pub(crate) struct DlcMessage { pub inbound: bool, pub peer_id: String, pub message_type: MessageType, - pub message_sub_type: MessageSubType, pub timestamp: OffsetDateTime, } @@ -103,7 +85,6 @@ impl From for DlcMessage { message_hash: value.message_hash.to_string(), peer_id: value.peer_id.to_string(), message_type: MessageType::from(value.clone().message_type), - message_sub_type: MessageSubType::from(value.message_type), timestamp: value.timestamp, inbound: value.inbound, } @@ -113,97 +94,59 @@ impl From for DlcMessage { impl From for MessageType { fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { match value { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(_) => Self::OnChain, - ln_dlc_node::dlc_message::DlcMessageType::Channel(_) => Self::Channel, - } - } -} - -impl From for MessageSubType { - fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { - let message_sub_type = match value { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(message_sub_type) => message_sub_type, - ln_dlc_node::dlc_message::DlcMessageType::Channel(message_sub_type) => message_sub_type, - }; - MessageSubType::from(message_sub_type) - } -} - -impl From for MessageSubType { - fn from(value: ln_dlc_node::dlc_message::DlcMessageSubType) -> Self { - match value { - ln_dlc_node::dlc_message::DlcMessageSubType::Offer => Self::Offer, - ln_dlc_node::dlc_message::DlcMessageSubType::Accept => Self::Accept, - ln_dlc_node::dlc_message::DlcMessageSubType::Sign => Self::Sign, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer => Self::SettleOffer, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept => Self::SettleAccept, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm => Self::SettleConfirm, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize => Self::SettleFinalize, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer => Self::RenewOffer, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept => Self::RenewAccept, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm => Self::RenewConfirm, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize => Self::RenewFinalize, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke => Self::RenewRevoke, - ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageType::Offer => Self::Offer, + ln_dlc_node::dlc_message::DlcMessageType::Accept => Self::Accept, + ln_dlc_node::dlc_message::DlcMessageType::Sign => Self::Sign, + ln_dlc_node::dlc_message::DlcMessageType::SettleOffer => Self::SettleOffer, + ln_dlc_node::dlc_message::DlcMessageType::SettleAccept => Self::SettleAccept, + ln_dlc_node::dlc_message::DlcMessageType::SettleConfirm => Self::SettleConfirm, + ln_dlc_node::dlc_message::DlcMessageType::SettleFinalize => Self::SettleFinalize, + ln_dlc_node::dlc_message::DlcMessageType::RenewOffer => Self::RenewOffer, + ln_dlc_node::dlc_message::DlcMessageType::RenewAccept => Self::RenewAccept, + ln_dlc_node::dlc_message::DlcMessageType::RenewConfirm => Self::RenewConfirm, + ln_dlc_node::dlc_message::DlcMessageType::RenewFinalize => Self::RenewFinalize, + ln_dlc_node::dlc_message::DlcMessageType::RenewRevoke => Self::RenewRevoke, + ln_dlc_node::dlc_message::DlcMessageType::CollaborativeCloseOffer => { Self::CollaborativeCloseOffer } - ln_dlc_node::dlc_message::DlcMessageSubType::Reject => Self::Reject, + ln_dlc_node::dlc_message::DlcMessageType::Reject => Self::Reject, } } } impl From for ln_dlc_node::dlc_message::DlcMessage { fn from(value: DlcMessage) -> Self { - let dlc_message_sub_type = - ln_dlc_node::dlc_message::DlcMessageSubType::from(value.clone().message_sub_type); - let dlc_message_type = match &value.message_type { - MessageType::OnChain => { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) - } - MessageType::Channel => { - ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) - } - }; - Self { message_hash: u64::from_str(&value.message_hash).expect("valid u64"), inbound: value.inbound, - message_type: dlc_message_type, + message_type: ln_dlc_node::dlc_message::DlcMessageType::from( + value.clone().message_type, + ), peer_id: PublicKey::from_str(&value.peer_id).expect("valid public key"), timestamp: value.timestamp, } } } -impl From for ln_dlc_node::dlc_message::DlcMessageSubType { - fn from(value: MessageSubType) -> Self { +impl From for ln_dlc_node::dlc_message::DlcMessageType { + fn from(value: MessageType) -> Self { match value { - MessageSubType::Offer => ln_dlc_node::dlc_message::DlcMessageSubType::Offer, - MessageSubType::Accept => ln_dlc_node::dlc_message::DlcMessageSubType::Accept, - MessageSubType::Sign => ln_dlc_node::dlc_message::DlcMessageSubType::Sign, - MessageSubType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer, - MessageSubType::SettleAccept => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept - } - MessageSubType::SettleConfirm => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm - } - MessageSubType::SettleFinalize => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize - } - MessageSubType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer, - MessageSubType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept, - MessageSubType::RenewConfirm => { - ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm - } - MessageSubType::RenewFinalize => { - ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize - } - MessageSubType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke, - MessageSubType::CollaborativeCloseOffer => { - ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer + MessageType::Offer => ln_dlc_node::dlc_message::DlcMessageType::Offer, + MessageType::Accept => ln_dlc_node::dlc_message::DlcMessageType::Accept, + MessageType::Sign => ln_dlc_node::dlc_message::DlcMessageType::Sign, + MessageType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageType::SettleOffer, + MessageType::SettleAccept => ln_dlc_node::dlc_message::DlcMessageType::SettleAccept, + MessageType::SettleConfirm => ln_dlc_node::dlc_message::DlcMessageType::SettleConfirm, + MessageType::SettleFinalize => ln_dlc_node::dlc_message::DlcMessageType::SettleFinalize, + MessageType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageType::RenewOffer, + MessageType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageType::RenewAccept, + MessageType::RenewConfirm => ln_dlc_node::dlc_message::DlcMessageType::RenewConfirm, + MessageType::RenewFinalize => ln_dlc_node::dlc_message::DlcMessageType::RenewFinalize, + MessageType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageType::RenewRevoke, + MessageType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageType::CollaborativeCloseOffer } - MessageSubType::Reject => ln_dlc_node::dlc_message::DlcMessageSubType::Reject, + MessageType::Reject => ln_dlc_node::dlc_message::DlcMessageType::Reject, } } } diff --git a/coordinator/src/db/last_outbound_dlc_message.rs b/coordinator/src/db/last_outbound_dlc_message.rs index 078defbbe..92d82fa36 100644 --- a/coordinator/src/db/last_outbound_dlc_message.rs +++ b/coordinator/src/db/last_outbound_dlc_message.rs @@ -1,4 +1,3 @@ -use crate::db::dlc_messages::MessageSubType; use crate::db::dlc_messages::MessageType; use crate::schema; use crate::schema::dlc_messages; @@ -41,32 +40,16 @@ pub(crate) fn get( .filter(last_outbound_dlc_messages::peer_id.eq(peer_id.to_string())) .select(( dlc_messages::message_type, - dlc_messages::message_sub_type, last_outbound_dlc_messages::message, )) - .first::<(MessageType, MessageSubType, String)>(conn) + .first::<(MessageType, String)>(conn) .optional()?; - let serialized_dlc_message = match last_outbound_dlc_message { - Some((message_type, message_sub_type, message)) => { - let dlc_message_sub_type = - ln_dlc_node::dlc_message::DlcMessageSubType::from(message_sub_type); - let message_type = match &message_type { - MessageType::OnChain => { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) - } - MessageType::Channel => { - ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) - } - }; - - Some(SerializedDlcMessage { - message, - message_type, - }) - } - None => None, - }; + let serialized_dlc_message = + last_outbound_dlc_message.map(|(message_type, message)| SerializedDlcMessage { + message, + message_type: ln_dlc_node::dlc_message::DlcMessageType::from(message_type), + }); Ok(serialized_dlc_message) } diff --git a/coordinator/src/dlc_handler.rs b/coordinator/src/dlc_handler.rs index 1faecad91..265a3e7d8 100644 --- a/coordinator/src/dlc_handler.rs +++ b/coordinator/src/dlc_handler.rs @@ -116,56 +116,4 @@ impl DlcHandler { Ok(()) } - - // Returns either the dlc message step or return none, if the dlc message has already been - // processed. - pub fn start_dlc_message_step( - conn: &mut PgConnection, - msg: &Message, - peer_id: PublicKey, - ) -> Result> { - let serialized_inbound_message = SerializedDlcMessage::try_from(msg)?; - let inbound_msg = DlcMessage::new(peer_id, serialized_inbound_message, true)?; - - let dlc_message_step = match db::dlc_messages::get(conn, inbound_msg.message_hash)? { - Some(_) => None, // the dlc message has already been processed, no step necessary. - None => Some(DlcMessageStep { - peer_id, - inbound_msg, - }), - }; - - Ok(dlc_message_step) - } -} - -pub struct DlcMessageStep { - pub peer_id: PublicKey, - pub inbound_msg: DlcMessage, -} - -impl DlcMessageStep { - /// Finishes the current dlc step by storing the received inbound message as processed and - /// caching the last outbound dlc message (if any) into the database. - pub fn finish(&self, conn: &mut PgConnection, response: &Option) -> Result<()> { - tracing::debug!("Marking the received message as processed"); - - db::dlc_messages::insert(conn, self.inbound_msg.clone())?; - - if let Some(resp) = response { - tracing::debug!("Persisting last outbound dlc message"); - let serialized_outbound_message = SerializedDlcMessage::try_from(resp)?; - let outbound_msg = - DlcMessage::new(self.peer_id, serialized_outbound_message.clone(), false)?; - - db::dlc_messages::insert(conn, outbound_msg)?; - db::last_outbound_dlc_message::upsert( - conn, - &self.peer_id, - serialized_outbound_message, - )?; - } - - Ok(()) - } } diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 51a0fce0d..72218967b 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -1,7 +1,6 @@ use crate::compute_relative_contracts; use crate::db; use crate::decimal_from_f32; -use crate::dlc_handler::DlcHandler; use crate::node::storage::NodeStorage; use crate::orderbook::db::matches; use crate::orderbook::db::orders; @@ -34,14 +33,14 @@ use dlc_manager::ContractId; use dlc_manager::DlcChannelId; use dlc_messages::ChannelMessage; use dlc_messages::Message; -use dlc_messages::SubChannelMessage; use lightning::ln::channelmanager::ChannelDetails; use lightning::ln::ChannelId; use lightning::util::config::UserConfig; +use ln_dlc_node::dlc_message::DlcMessage; +use ln_dlc_node::dlc_message::SerializedDlcMessage; use ln_dlc_node::node; use ln_dlc_node::node::dlc_message_name; -use ln_dlc_node::node::send_sub_channel_message; -use ln_dlc_node::node::sub_channel_message_name; +use ln_dlc_node::node::event::NodeEvent; use ln_dlc_node::node::RunningNode; use ln_dlc_node::WalletSettings; use rust_decimal::prelude::ToPrimitive; @@ -600,18 +599,21 @@ impl Node { ); let resp = match &msg { - Message::OnChain(_) | Message::Channel(_) => { - let dlc_message_step = { + Message::OnChain(_) | Message::SubChannel(_) => { + tracing::warn!(from = %node_id, kind = %dlc_message_name(&msg),"Ignoring unexpected dlc message."); + None + } + Message::Channel(channel_msg) => { + let inbound_msg = { let mut conn = self.pool.get()?; - let dlc_message_step = - DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; - - match dlc_message_step { - Some(dlc_message_step) => dlc_message_step, - None => { + let serialized_inbound_message = SerializedDlcMessage::try_from(&msg)?; + let inbound_msg = DlcMessage::new(node_id, serialized_inbound_message, true)?; + match db::dlc_messages::get(&mut conn, inbound_msg.message_hash)? { + Some(_) => { tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); return Ok(()); } + None => inbound_msg, } }; @@ -628,47 +630,58 @@ impl Node { { let mut conn = self.pool.get()?; - dlc_message_step.finish(&mut conn, &resp)?; + db::dlc_messages::insert(&mut conn, inbound_msg)?; } + match channel_msg { + ChannelMessage::RenewFinalize(r) => self.finalize_rollover(&r.channel_id)?, + ChannelMessage::SettleFinalize(settle_finalize) => { + let channel_id_hex_string = settle_finalize.channel_id.to_hex(); + tracing::info!( + channel_id = channel_id_hex_string, + node_id = node_id.to_string(), + "DLC channel settle protocol was finalized" + ); + let mut connection = self.pool.get()?; + + match db::positions::Position::get_position_by_trader( + &mut connection, + node_id, + vec![ + // The price doesn't matter here. + PositionState::Closing { closing_price: 0.0 }, + ], + )? { + None => { + tracing::error!( + channel_id = channel_id_hex_string, + "No position in Closing state found" + ); + } + Some(position) => { + self.finalize_closing_position(&mut connection, position)?; + } + } + } + ChannelMessage::CollaborativeCloseOffer(close_offer) => { + let channel_id_hex_string = close_offer.channel_id.to_hex(); + tracing::info!( + channel_id = channel_id_hex_string, + node_id = node_id.to_string(), + "Received an offer to collaboratively close a channel" + ); + + // TODO(bonomat): we should verify that the proposed amount is acceptable + self.inner + .accept_dlc_channel_collaborative_close(close_offer.channel_id)?; + } + _ => {} + }; + resp } - Message::SubChannel(msg) => self - .inner - .sub_channel_manager - .on_sub_channel_message(msg, &node_id) - .with_context(|| { - format!( - "Failed to handle {} message from {node_id}", - sub_channel_message_name(msg) - ) - })? - .map(Message::SubChannel), }; - // TODO(holzeis): It would be nice if dlc messages are also propagated via events, so the - // receiver can decide what events to process and we can skip this component specific logic - // here. - // Note: The NodeEventHandler could be used for that! - if let Message::Channel(ChannelMessage::RenewFinalize(r)) = &msg { - self.finalize_rollover(&r.channel_id)?; - } - - if let Message::SubChannel(SubChannelMessage::Finalize(finalize)) = &msg { - let channel_id_hex_string = finalize.channel_id.to_hex(); - tracing::info!( - channel_id = channel_id_hex_string, - node_id = node_id.to_string(), - "Subchannel open protocol was finalized" - ); - let mut connection = self.pool.get()?; - db::positions::Position::update_proposed_position( - &mut connection, - node_id.to_string(), - PositionState::Open, - )?; - } - if let Some(Message::Channel(ChannelMessage::Sign(sign_channel))) = &resp { let channel_id_hex_string = sign_channel.channel_id.to_hex(); tracing::info!( @@ -684,118 +697,6 @@ impl Node { )?; } - if let Message::Channel(ChannelMessage::SettleFinalize(settle_finalize)) = &msg { - let channel_id_hex_string = settle_finalize.channel_id.to_hex(); - tracing::info!( - channel_id = channel_id_hex_string, - node_id = node_id.to_string(), - "DLC channel settle protocol was finalized" - ); - let mut connection = self.pool.get()?; - - match db::positions::Position::get_position_by_trader( - &mut connection, - node_id, - vec![ - // The price doesn't matter here. - PositionState::Closing { closing_price: 0.0 }, - ], - )? { - None => { - tracing::error!( - channel_id = channel_id_hex_string, - "No position in Closing state found" - ); - } - Some(position) => { - self.finalize_closing_position(&mut connection, position)?; - } - } - } - - if let Message::Channel(ChannelMessage::CollaborativeCloseOffer(close_offer)) = &msg { - let channel_id_hex_string = close_offer.channel_id.to_hex(); - tracing::info!( - channel_id = channel_id_hex_string, - node_id = node_id.to_string(), - "Received an offer to collaboratively close a channel" - ); - - // TODO(bonomat): we should verify that the proposed amount is acceptable - self.inner - .accept_dlc_channel_collaborative_close(close_offer.channel_id)?; - } - - if let Message::SubChannel(SubChannelMessage::CloseFinalize(msg)) = &msg { - let mut connection = self.pool.get()?; - match db::positions::Position::get_position_by_trader( - &mut connection, - node_id, - vec![ - // the price doesn't matter here - PositionState::Closing { closing_price: 0.0 }, - PositionState::Resizing, - ], - )? { - None => { - tracing::warn!( - channel_id = msg.channel_id.to_hex(), - "No position found to finalize" - ); - } - Some(position) => match position.position_state { - PositionState::Closing { .. } => { - self.finalize_closing_position(&mut connection, position)?; - } - PositionState::Resizing => { - self.continue_position_resizing(node_id, position)?; - } - state => { - // this should never happen because we are only loading specific states - tracing::error!( - channel_id = msg.channel_id.to_hex(), - position_id = position.id, - position_state = ?state, - "Position was in unexpected state when trying to finalize the subchannel" - ); - } - }, - } - } - - if let Message::SubChannel(SubChannelMessage::Reject(reject)) = &msg { - let channel_id_hex = reject.channel_id.to_hex(); - tracing::warn!(channel_id = channel_id_hex, "Subchannel offer was rejected"); - let mut connection = self.pool.get()?; - match db::positions::Position::get_position_by_trader( - &mut connection, - node_id, - vec![ - PositionState::Proposed, - PositionState::ResizeOpeningSubchannelProposed, - ], - )? { - None => { - tracing::warn!("No position found to be updated") - } - Some(position) => { - let updated_state = match position.position_state { - PositionState::Proposed => PositionState::Failed, - PositionState::ResizeOpeningSubchannelProposed => PositionState::Open, - state => { - // This should not happen because we only load these two states above - bail!("Position was in unexpected state {state:?}."); - } - }; - db::positions::Position::update_proposed_position( - &mut connection, - node_id.to_string(), - updated_state, - )?; - } - } - } - if let Some(msg) = resp { tracing::info!( to = %node_id, @@ -803,12 +704,9 @@ impl Node { "Sending message" ); - send_sub_channel_message( - &self.inner.dlc_message_handler, - &self.inner.peer_manager, - node_id, - msg, - ); + self.inner + .event_handler + .publish(NodeEvent::SendDlcMessage { peer: node_id, msg })?; } Ok(()) diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index dd720b14c..dc1aaebb2 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -21,10 +21,6 @@ pub mod sql_types { #[diesel(postgres_type(name = "MatchState_Type"))] pub struct MatchStateType; - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "Message_Sub_Type_Type"))] - pub struct MessageSubTypeType; - #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "Message_Type_Type"))] pub struct MessageTypeType; @@ -87,14 +83,12 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use super::sql_types::MessageTypeType; - use super::sql_types::MessageSubTypeType; dlc_messages (message_hash) { message_hash -> Text, inbound -> Bool, peer_id -> Text, message_type -> MessageTypeType, - message_sub_type -> MessageSubTypeType, timestamp -> Timestamptz, } } diff --git a/crates/ln-dlc-node/src/dlc_message.rs b/crates/ln-dlc-node/src/dlc_message.rs index 2b4c21665..9d6eea78e 100644 --- a/crates/ln-dlc-node/src/dlc_message.rs +++ b/crates/ln-dlc-node/src/dlc_message.rs @@ -2,7 +2,6 @@ use anyhow::Result; use bitcoin::secp256k1::PublicKey; use dlc_messages::ChannelMessage; use dlc_messages::Message; -use dlc_messages::OnChainMessage; use std::collections::hash_map::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; @@ -52,12 +51,6 @@ impl SerializedDlcMessage { #[derive(Hash, Clone, Debug)] pub enum DlcMessageType { - OnChain(DlcMessageSubType), - Channel(DlcMessageSubType), -} - -#[derive(Hash, Clone, Debug)] -pub enum DlcMessageSubType { Offer, Accept, Sign, @@ -79,78 +72,50 @@ impl TryFrom<&SerializedDlcMessage> for Message { fn try_from(serialized_msg: &SerializedDlcMessage) -> Result { let message = match serialized_msg.clone().message_type { - DlcMessageType::OnChain(serialized_onchain_message_type) => { - match serialized_onchain_message_type { - DlcMessageSubType::Offer => Message::OnChain(OnChainMessage::Offer( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::Accept => Message::OnChain(OnChainMessage::Accept( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::Sign => Message::OnChain(OnChainMessage::Sign( - serde_json::from_str(&serialized_msg.message)?, - )), - _ => unreachable!(), - } - } - DlcMessageType::Channel(serialized_channel_message_type) => { - match serialized_channel_message_type { - DlcMessageSubType::Offer => Message::Channel(ChannelMessage::Offer( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::Accept => Message::Channel(ChannelMessage::Accept( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::Sign => Message::Channel(ChannelMessage::Sign( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::SettleOffer => Message::Channel( - ChannelMessage::SettleOffer(serde_json::from_str(&serialized_msg.message)?), - ), - DlcMessageSubType::SettleAccept => { - Message::Channel(ChannelMessage::SettleAccept(serde_json::from_str( - &serialized_msg.message, - )?)) - } - DlcMessageSubType::SettleConfirm => { - Message::Channel(ChannelMessage::SettleConfirm(serde_json::from_str( - &serialized_msg.message, - )?)) - } - DlcMessageSubType::SettleFinalize => { - Message::Channel(ChannelMessage::SettleFinalize(serde_json::from_str( - &serialized_msg.message, - )?)) - } - DlcMessageSubType::RenewOffer => Message::Channel(ChannelMessage::RenewOffer( - serde_json::from_str(&serialized_msg.message)?, - )), - DlcMessageSubType::RenewAccept => Message::Channel( - ChannelMessage::RenewAccept(serde_json::from_str(&serialized_msg.message)?), - ), - DlcMessageSubType::RenewConfirm => { - Message::Channel(ChannelMessage::RenewConfirm(serde_json::from_str( - &serialized_msg.message, - )?)) - } - DlcMessageSubType::RenewFinalize => { - Message::Channel(ChannelMessage::RenewFinalize(serde_json::from_str( - &serialized_msg.message, - )?)) - } - DlcMessageSubType::RenewRevoke => Message::Channel( - ChannelMessage::RenewRevoke(serde_json::from_str(&serialized_msg.message)?), - ), - DlcMessageSubType::CollaborativeCloseOffer => { - Message::Channel(ChannelMessage::CollaborativeCloseOffer( - serde_json::from_str(&serialized_msg.message)?, - )) - } - DlcMessageSubType::Reject => Message::Channel(ChannelMessage::Reject( - serde_json::from_str(&serialized_msg.message)?, - )), - } + DlcMessageType::Offer => Message::Channel(ChannelMessage::Offer(serde_json::from_str( + &serialized_msg.message, + )?)), + DlcMessageType::Accept => Message::Channel(ChannelMessage::Accept( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::Sign => Message::Channel(ChannelMessage::Sign(serde_json::from_str( + &serialized_msg.message, + )?)), + DlcMessageType::SettleOffer => Message::Channel(ChannelMessage::SettleOffer( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::SettleAccept => Message::Channel(ChannelMessage::SettleAccept( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::SettleConfirm => Message::Channel(ChannelMessage::SettleConfirm( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::SettleFinalize => Message::Channel(ChannelMessage::SettleFinalize( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::RenewOffer => Message::Channel(ChannelMessage::RenewOffer( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::RenewAccept => Message::Channel(ChannelMessage::RenewAccept( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::RenewConfirm => Message::Channel(ChannelMessage::RenewConfirm( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::RenewFinalize => Message::Channel(ChannelMessage::RenewFinalize( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::RenewRevoke => Message::Channel(ChannelMessage::RenewRevoke( + serde_json::from_str(&serialized_msg.message)?, + )), + DlcMessageType::CollaborativeCloseOffer => { + Message::Channel(ChannelMessage::CollaborativeCloseOffer( + serde_json::from_str(&serialized_msg.message)?, + )) } + DlcMessageType::Reject => Message::Channel(ChannelMessage::Reject( + serde_json::from_str(&serialized_msg.message)?, + )), }; Ok(message) @@ -162,77 +127,57 @@ impl TryFrom<&Message> for SerializedDlcMessage { fn try_from(msg: &Message) -> Result { let (message, message_type) = match &msg { - Message::OnChain(message) => match message { - OnChainMessage::Offer(offer) => ( - serde_json::to_string(&offer)?, - DlcMessageType::OnChain(DlcMessageSubType::Offer), - ), - OnChainMessage::Accept(accept) => ( - serde_json::to_string(&accept)?, - DlcMessageType::OnChain(DlcMessageSubType::Accept), - ), - OnChainMessage::Sign(sign) => ( - serde_json::to_string(&sign)?, - DlcMessageType::OnChain(DlcMessageSubType::Sign), - ), - }, Message::Channel(message) => match message { - ChannelMessage::Offer(offer) => ( - serde_json::to_string(&offer)?, - DlcMessageType::Channel(DlcMessageSubType::Offer), - ), - ChannelMessage::Accept(accept) => ( - serde_json::to_string(&accept)?, - DlcMessageType::Channel(DlcMessageSubType::Accept), - ), - ChannelMessage::Sign(sign) => ( - serde_json::to_string(&sign)?, - DlcMessageType::Channel(DlcMessageSubType::Sign), - ), + ChannelMessage::Offer(offer) => { + (serde_json::to_string(&offer)?, DlcMessageType::Offer) + } + ChannelMessage::Accept(accept) => { + (serde_json::to_string(&accept)?, DlcMessageType::Accept) + } + ChannelMessage::Sign(sign) => (serde_json::to_string(&sign)?, DlcMessageType::Sign), ChannelMessage::SettleOffer(settle_offer) => ( serde_json::to_string(&settle_offer)?, - DlcMessageType::Channel(DlcMessageSubType::SettleOffer), + DlcMessageType::SettleOffer, ), ChannelMessage::SettleAccept(settle_accept) => ( serde_json::to_string(&settle_accept)?, - DlcMessageType::Channel(DlcMessageSubType::SettleAccept), + DlcMessageType::SettleAccept, ), ChannelMessage::SettleConfirm(settle_confirm) => ( serde_json::to_string(&settle_confirm)?, - DlcMessageType::Channel(DlcMessageSubType::SettleConfirm), + DlcMessageType::SettleConfirm, ), ChannelMessage::SettleFinalize(settle_finalize) => ( serde_json::to_string(&settle_finalize)?, - DlcMessageType::Channel(DlcMessageSubType::SettleFinalize), + DlcMessageType::SettleFinalize, ), ChannelMessage::RenewOffer(renew_offer) => ( serde_json::to_string(&renew_offer)?, - DlcMessageType::Channel(DlcMessageSubType::RenewOffer), + DlcMessageType::RenewOffer, ), ChannelMessage::RenewAccept(renew_accept) => ( serde_json::to_string(&renew_accept)?, - DlcMessageType::Channel(DlcMessageSubType::RenewAccept), + DlcMessageType::RenewAccept, ), ChannelMessage::RenewConfirm(renew_confirm) => ( serde_json::to_string(&renew_confirm)?, - DlcMessageType::Channel(DlcMessageSubType::RenewConfirm), + DlcMessageType::RenewConfirm, ), ChannelMessage::RenewFinalize(renew_finalize) => ( serde_json::to_string(&renew_finalize)?, - DlcMessageType::Channel(DlcMessageSubType::RenewFinalize), + DlcMessageType::RenewFinalize, ), ChannelMessage::RenewRevoke(renew_revoke) => ( serde_json::to_string(&renew_revoke)?, - DlcMessageType::Channel(DlcMessageSubType::RenewRevoke), + DlcMessageType::RenewRevoke, ), ChannelMessage::CollaborativeCloseOffer(collaborative_close_offer) => ( serde_json::to_string(&collaborative_close_offer)?, - DlcMessageType::Channel(DlcMessageSubType::CollaborativeCloseOffer), - ), - ChannelMessage::Reject(reject) => ( - serde_json::to_string(&reject)?, - DlcMessageType::Channel(DlcMessageSubType::Reject), + DlcMessageType::CollaborativeCloseOffer, ), + ChannelMessage::Reject(reject) => { + (serde_json::to_string(&reject)?, DlcMessageType::Reject) + } }, _ => unreachable!(), }; diff --git a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql index 29d48d63e..38ffd35e2 100644 --- a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql +++ b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -4,7 +4,6 @@ CREATE TABLE "dlc_messages" ( inbound BOOLEAN NOT NULL, peer_id TEXT NOT NULL, message_type TEXT NOT NULL, - message_sub_type TEXT NOT NULL, timestamp BIGINT NOT NULL ); diff --git a/mobile/native/src/db/custom_types.rs b/mobile/native/src/db/custom_types.rs index 5d711d7f4..962fb593b 100644 --- a/mobile/native/src/db/custom_types.rs +++ b/mobile/native/src/db/custom_types.rs @@ -1,4 +1,3 @@ -use crate::db::dlc_messages::MessageSubType; use crate::db::dlc_messages::MessageType; use crate::db::models::ChannelState; use crate::db::models::ContractSymbol; @@ -270,8 +269,20 @@ impl FromSql for ChannelState { impl ToSql for MessageType { fn to_sql(&self, out: &mut Output) -> serialize::Result { let text = match *self { - MessageType::OnChain => "OnChain", - MessageType::Channel => "Channel", + MessageType::Offer => "Offer", + MessageType::Accept => "Accept", + MessageType::Sign => "Sign", + MessageType::SettleOffer => "SettleOffer", + MessageType::SettleAccept => "SettleAccept", + MessageType::SettleConfirm => "SettleConfirm", + MessageType::SettleFinalize => "SettleFinalize", + MessageType::RenewOffer => "RenewOffer", + MessageType::RenewAccept => "RenewAccept", + MessageType::RenewConfirm => "RenewConfirm", + MessageType::RenewFinalize => "RenewFinalize", + MessageType::RenewRevoke => "RenewRevoke", + MessageType::CollaborativeCloseOffer => "CollaborativeCloseOffer", + MessageType::Reject => "Reject", }; out.set_value(text); Ok(IsNull::No) @@ -283,55 +294,20 @@ impl FromSql for MessageType { let string = >::from_sql(bytes)?; return match string.as_str() { - "OnChain" => Ok(MessageType::OnChain), - "Channel" => Ok(MessageType::Channel), - _ => Err("Unrecognized enum variant".into()), - }; - } -} - -impl ToSql for MessageSubType { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - let text = match *self { - MessageSubType::Offer => "Offer", - MessageSubType::Accept => "Accept", - MessageSubType::Sign => "Sign", - MessageSubType::SettleOffer => "SettleOffer", - MessageSubType::SettleAccept => "SettleAccept", - MessageSubType::SettleConfirm => "SettleConfirm", - MessageSubType::SettleFinalize => "SettleFinalize", - MessageSubType::RenewOffer => "RenewOffer", - MessageSubType::RenewAccept => "RenewAccept", - MessageSubType::RenewConfirm => "RenewConfirm", - MessageSubType::RenewFinalize => "RenewFinalize", - MessageSubType::RenewRevoke => "RenewRevoke", - MessageSubType::CollaborativeCloseOffer => "CollaborativeCloseOffer", - MessageSubType::Reject => "Reject", - }; - out.set_value(text); - Ok(IsNull::No) - } -} - -impl FromSql for MessageSubType { - fn from_sql(bytes: backend::RawValue) -> deserialize::Result { - let string = >::from_sql(bytes)?; - - return match string.as_str() { - "Offer" => Ok(MessageSubType::Offer), - "Accept" => Ok(MessageSubType::Accept), - "Sign" => Ok(MessageSubType::Sign), - "SettleOffer" => Ok(MessageSubType::SettleOffer), - "SettleAccept" => Ok(MessageSubType::SettleAccept), - "SettleConfirm" => Ok(MessageSubType::SettleConfirm), - "SettleFinalize" => Ok(MessageSubType::SettleFinalize), - "RenewOffer" => Ok(MessageSubType::RenewOffer), - "RenewAccept" => Ok(MessageSubType::RenewAccept), - "RenewConfirm" => Ok(MessageSubType::RenewConfirm), - "RenewFinalize" => Ok(MessageSubType::RenewFinalize), - "RenewRevoke" => Ok(MessageSubType::RenewRevoke), - "CollaborativeCloseOffer" => Ok(MessageSubType::CollaborativeCloseOffer), - "Reject" => Ok(MessageSubType::Reject), + "Offer" => Ok(MessageType::Offer), + "Accept" => Ok(MessageType::Accept), + "Sign" => Ok(MessageType::Sign), + "SettleOffer" => Ok(MessageType::SettleOffer), + "SettleAccept" => Ok(MessageType::SettleAccept), + "SettleConfirm" => Ok(MessageType::SettleConfirm), + "SettleFinalize" => Ok(MessageType::SettleFinalize), + "RenewOffer" => Ok(MessageType::RenewOffer), + "RenewAccept" => Ok(MessageType::RenewAccept), + "RenewConfirm" => Ok(MessageType::RenewConfirm), + "RenewFinalize" => Ok(MessageType::RenewFinalize), + "RenewRevoke" => Ok(MessageType::RenewRevoke), + "CollaborativeCloseOffer" => Ok(MessageType::CollaborativeCloseOffer), + "Reject" => Ok(MessageType::Reject), _ => Err("Unrecognized enum variant".into()), }; } diff --git a/mobile/native/src/db/dlc_messages.rs b/mobile/native/src/db/dlc_messages.rs index ae5e26ceb..ba3bd75cc 100644 --- a/mobile/native/src/db/dlc_messages.rs +++ b/mobile/native/src/db/dlc_messages.rs @@ -25,20 +25,12 @@ pub(crate) struct DlcMessage { pub inbound: bool, pub peer_id: String, pub message_type: MessageType, - pub message_sub_type: MessageSubType, pub timestamp: i64, } #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] #[diesel(sql_type = Text)] pub enum MessageType { - OnChain, - Channel, -} - -#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] -#[diesel(sql_type = Text)] -pub enum MessageSubType { Offer, Accept, Sign, @@ -76,7 +68,7 @@ impl DlcMessage { .values(DlcMessage::from(dlc_message)) .execute(conn)?; - ensure!(affected_rows > 0, "Could not insert queue"); + ensure!(affected_rows > 0, "Could not insert dlc message"); Ok(()) } @@ -88,7 +80,6 @@ impl From for DlcMessage { message_hash: value.message_hash.to_string(), peer_id: value.peer_id.to_string(), message_type: MessageType::from(value.clone().message_type), - message_sub_type: MessageSubType::from(value.message_type), timestamp: value.timestamp.unix_timestamp(), inbound: value.inbound, } @@ -98,57 +89,30 @@ impl From for DlcMessage { impl From for MessageType { fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { match value { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(_) => Self::OnChain, - ln_dlc_node::dlc_message::DlcMessageType::Channel(_) => Self::Channel, - } - } -} - -impl From for MessageSubType { - fn from(value: ln_dlc_node::dlc_message::DlcMessageType) -> Self { - let message_sub_type = match value { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(message_sub_type) => message_sub_type, - ln_dlc_node::dlc_message::DlcMessageType::Channel(message_sub_type) => message_sub_type, - }; - MessageSubType::from(message_sub_type) - } -} - -impl From for MessageSubType { - fn from(value: ln_dlc_node::dlc_message::DlcMessageSubType) -> Self { - match value { - ln_dlc_node::dlc_message::DlcMessageSubType::Offer => Self::Offer, - ln_dlc_node::dlc_message::DlcMessageSubType::Accept => Self::Accept, - ln_dlc_node::dlc_message::DlcMessageSubType::Sign => Self::Sign, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer => Self::SettleOffer, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept => Self::SettleAccept, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm => Self::SettleConfirm, - ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize => Self::SettleFinalize, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer => Self::RenewOffer, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept => Self::RenewAccept, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm => Self::RenewConfirm, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize => Self::RenewFinalize, - ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke => Self::RenewRevoke, - ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageType::Offer => Self::Offer, + ln_dlc_node::dlc_message::DlcMessageType::Accept => Self::Accept, + ln_dlc_node::dlc_message::DlcMessageType::Sign => Self::Sign, + ln_dlc_node::dlc_message::DlcMessageType::SettleOffer => Self::SettleOffer, + ln_dlc_node::dlc_message::DlcMessageType::SettleAccept => Self::SettleAccept, + ln_dlc_node::dlc_message::DlcMessageType::SettleConfirm => Self::SettleConfirm, + ln_dlc_node::dlc_message::DlcMessageType::SettleFinalize => Self::SettleFinalize, + ln_dlc_node::dlc_message::DlcMessageType::RenewOffer => Self::RenewOffer, + ln_dlc_node::dlc_message::DlcMessageType::RenewAccept => Self::RenewAccept, + ln_dlc_node::dlc_message::DlcMessageType::RenewConfirm => Self::RenewConfirm, + ln_dlc_node::dlc_message::DlcMessageType::RenewFinalize => Self::RenewFinalize, + ln_dlc_node::dlc_message::DlcMessageType::RenewRevoke => Self::RenewRevoke, + ln_dlc_node::dlc_message::DlcMessageType::CollaborativeCloseOffer => { Self::CollaborativeCloseOffer } - ln_dlc_node::dlc_message::DlcMessageSubType::Reject => Self::Reject, + ln_dlc_node::dlc_message::DlcMessageType::Reject => Self::Reject, } } } impl From for ln_dlc_node::dlc_message::DlcMessage { fn from(value: DlcMessage) -> Self { - let dlc_message_sub_type = - ln_dlc_node::dlc_message::DlcMessageSubType::from(value.clone().message_sub_type); - let dlc_message_type = match &value.message_type { - MessageType::OnChain => { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) - } - MessageType::Channel => { - ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) - } - }; + let dlc_message_type = + ln_dlc_node::dlc_message::DlcMessageType::from(value.clone().message_type); Self { message_hash: u64::from_str(&value.message_hash).expect("valid u64"), @@ -161,35 +125,25 @@ impl From for ln_dlc_node::dlc_message::DlcMessage { } } -impl From for ln_dlc_node::dlc_message::DlcMessageSubType { - fn from(value: MessageSubType) -> Self { +impl From for ln_dlc_node::dlc_message::DlcMessageType { + fn from(value: MessageType) -> Self { match value { - MessageSubType::Offer => ln_dlc_node::dlc_message::DlcMessageSubType::Offer, - MessageSubType::Accept => ln_dlc_node::dlc_message::DlcMessageSubType::Accept, - MessageSubType::Sign => ln_dlc_node::dlc_message::DlcMessageSubType::Sign, - MessageSubType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageSubType::SettleOffer, - MessageSubType::SettleAccept => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleAccept - } - MessageSubType::SettleConfirm => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleConfirm - } - MessageSubType::SettleFinalize => { - ln_dlc_node::dlc_message::DlcMessageSubType::SettleFinalize - } - MessageSubType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageSubType::RenewOffer, - MessageSubType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageSubType::RenewAccept, - MessageSubType::RenewConfirm => { - ln_dlc_node::dlc_message::DlcMessageSubType::RenewConfirm - } - MessageSubType::RenewFinalize => { - ln_dlc_node::dlc_message::DlcMessageSubType::RenewFinalize - } - MessageSubType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageSubType::RenewRevoke, - MessageSubType::CollaborativeCloseOffer => { - ln_dlc_node::dlc_message::DlcMessageSubType::CollaborativeCloseOffer + MessageType::Offer => ln_dlc_node::dlc_message::DlcMessageType::Offer, + MessageType::Accept => ln_dlc_node::dlc_message::DlcMessageType::Accept, + MessageType::Sign => ln_dlc_node::dlc_message::DlcMessageType::Sign, + MessageType::SettleOffer => ln_dlc_node::dlc_message::DlcMessageType::SettleOffer, + MessageType::SettleAccept => ln_dlc_node::dlc_message::DlcMessageType::SettleAccept, + MessageType::SettleConfirm => ln_dlc_node::dlc_message::DlcMessageType::SettleConfirm, + MessageType::SettleFinalize => ln_dlc_node::dlc_message::DlcMessageType::SettleFinalize, + MessageType::RenewOffer => ln_dlc_node::dlc_message::DlcMessageType::RenewOffer, + MessageType::RenewAccept => ln_dlc_node::dlc_message::DlcMessageType::RenewAccept, + MessageType::RenewConfirm => ln_dlc_node::dlc_message::DlcMessageType::RenewConfirm, + MessageType::RenewFinalize => ln_dlc_node::dlc_message::DlcMessageType::RenewFinalize, + MessageType::RenewRevoke => ln_dlc_node::dlc_message::DlcMessageType::RenewRevoke, + MessageType::CollaborativeCloseOffer => { + ln_dlc_node::dlc_message::DlcMessageType::CollaborativeCloseOffer } - MessageSubType::Reject => ln_dlc_node::dlc_message::DlcMessageSubType::Reject, + MessageType::Reject => ln_dlc_node::dlc_message::DlcMessageType::Reject, } } } diff --git a/mobile/native/src/db/last_outbound_dlc_messages.rs b/mobile/native/src/db/last_outbound_dlc_messages.rs index c6b581616..518706850 100644 --- a/mobile/native/src/db/last_outbound_dlc_messages.rs +++ b/mobile/native/src/db/last_outbound_dlc_messages.rs @@ -1,4 +1,3 @@ -use crate::db::dlc_messages::MessageSubType; use crate::db::dlc_messages::MessageType; use crate::schema; use crate::schema::dlc_messages; @@ -41,32 +40,16 @@ impl LastOutboundDlcMessage { .filter(last_outbound_dlc_messages::peer_id.eq(peer_id.to_string())) .select(( dlc_messages::message_type, - dlc_messages::message_sub_type, last_outbound_dlc_messages::message, )) - .first::<(MessageType, MessageSubType, String)>(conn) + .first::<(MessageType, String)>(conn) .optional()?; - let serialized_dlc_message = match last_outbound_dlc_message { - Some((message_type, message_sub_type, message)) => { - let dlc_message_sub_type = - ln_dlc_node::dlc_message::DlcMessageSubType::from(message_sub_type); - let message_type = match &message_type { - MessageType::OnChain => { - ln_dlc_node::dlc_message::DlcMessageType::OnChain(dlc_message_sub_type) - } - MessageType::Channel => { - ln_dlc_node::dlc_message::DlcMessageType::Channel(dlc_message_sub_type) - } - }; - - Some(SerializedDlcMessage { - message, - message_type, - }) - } - None => None, - }; + let serialized_dlc_message = + last_outbound_dlc_message.map(|(message_type, message)| SerializedDlcMessage { + message, + message_type: ln_dlc_node::dlc_message::DlcMessageType::from(message_type), + }); Ok(serialized_dlc_message) } diff --git a/mobile/native/src/dlc_handler.rs b/mobile/native/src/dlc_handler.rs index a74a76593..9f5a131bb 100644 --- a/mobile/native/src/dlc_handler.rs +++ b/mobile/native/src/dlc_handler.rs @@ -3,7 +3,6 @@ use crate::ln_dlc::node::NodeStorage; use crate::storage::TenTenOneNodeStorage; use anyhow::Result; use bitcoin::secp256k1::PublicKey; -use diesel::SqliteConnection; use dlc_messages::Message; use ln_dlc_node::dlc_message::DlcMessage; use ln_dlc_node::dlc_message::SerializedDlcMessage; @@ -101,58 +100,6 @@ impl DlcHandler { tracing::debug!(%peer, "No last dlc message found. Nothing todo."); } - Ok(()) - } - // Returns either the dlc message step or return none, if the dlc message has already been - // processed. - pub fn start_dlc_message_step( - conn: &mut SqliteConnection, - msg: &Message, - peer_id: PublicKey, - ) -> Result> { - let serialized_inbound_message = SerializedDlcMessage::try_from(msg)?; - let inbound_msg = DlcMessage::new(peer_id, serialized_inbound_message, true)?; - - let dlc_message_step = - match db::dlc_messages::DlcMessage::get(conn, inbound_msg.message_hash)? { - Some(_) => None, // the dlc message has already been processed, no step necessary. - None => Some(DlcMessageStep { - inbound_msg, - peer_id, - }), - }; - - Ok(dlc_message_step) - } -} - -pub struct DlcMessageStep { - pub peer_id: PublicKey, - pub inbound_msg: DlcMessage, -} - -impl DlcMessageStep { - /// Finishes the current dlc step by storing the received inbound message as processed and - /// caching the last outbound dlc message (if any) into the database. - pub fn finish(&self, conn: &mut SqliteConnection, response: &Option) -> Result<()> { - tracing::debug!("Marking the received message as processed"); - - db::dlc_messages::DlcMessage::insert(conn, self.inbound_msg.clone())?; - - if let Some(resp) = response { - tracing::debug!("Persisting last outbound dlc message"); - let serialized_outbound_message = SerializedDlcMessage::try_from(resp)?; - let outbound_msg = - DlcMessage::new(self.peer_id, serialized_outbound_message.clone(), false)?; - - db::dlc_messages::DlcMessage::insert(conn, outbound_msg)?; - db::last_outbound_dlc_messages::LastOutboundDlcMessage::upsert( - conn, - &self.peer_id, - serialized_outbound_message, - )?; - } - Ok(()) } } diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index fe1efecf0..77b9721a3 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -1,5 +1,4 @@ use crate::db; -use crate::dlc_handler::DlcHandler; use crate::event; use crate::event::BackgroundTask; use crate::event::EventInternal; @@ -18,11 +17,8 @@ use bdk::TransactionDetails; use bitcoin::hashes::hex::ToHex; use bitcoin::Txid; use commons::order_matching_fee_taker; -use dlc_messages::sub_channel::SubChannelCloseFinalize; -use dlc_messages::sub_channel::SubChannelRevoke; use dlc_messages::ChannelMessage; use dlc_messages::Message; -use dlc_messages::SubChannelMessage; use lightning::chain::transaction::OutPoint; use lightning::ln::ChannelId; use lightning::ln::PaymentHash; @@ -32,10 +28,12 @@ use lightning::sign::DelayedPaymentOutputDescriptor; use lightning::sign::SpendableOutputDescriptor; use lightning::sign::StaticPaymentOutputDescriptor; use ln_dlc_node::channel::Channel; +use ln_dlc_node::dlc_message::DlcMessage; +use ln_dlc_node::dlc_message::SerializedDlcMessage; use ln_dlc_node::node; use ln_dlc_node::node::dlc_message_name; +use ln_dlc_node::node::event::NodeEvent; use ln_dlc_node::node::rust_dlc_manager::DlcChannelId; -use ln_dlc_node::node::sub_channel_message_name; use ln_dlc_node::node::NodeInfo; use ln_dlc_node::node::PaymentDetails; use ln_dlc_node::node::RunningNode; @@ -151,51 +149,21 @@ impl Node { ); let resp = match &msg { - Message::OnChain(_) => { - let dlc_message_step = { - let mut conn = db::connection()?; - let dlc_message_step = - DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; - - match dlc_message_step { - Some(dlc_message_step) => dlc_message_step, - None => { - tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); - return Ok(()); - } - } - }; - - let resp = self - .inner - .dlc_manager - .on_dlc_message(&msg, node_id) - .with_context(|| { - format!( - "Failed to handle {} message from {node_id}", - dlc_message_name(&msg) - ) - })?; - - { - let mut conn = db::connection()?; - dlc_message_step.finish(&mut conn, &resp)?; - } - - resp - } + Message::OnChain(_) | Message::SubChannel(_) => { + tracing::warn!("Ignoring unexpected dlc message."); + None + }, Message::Channel(channel_msg) => { - let dlc_message_step = { + let inbound_msg = { let mut conn = db::connection()?; - let dlc_message_step = - DlcHandler::start_dlc_message_step(&mut conn, &msg, node_id)?; - - match dlc_message_step { - Some(dlc_message_step) => dlc_message_step, - None => { + let serialized_inbound_message = SerializedDlcMessage::try_from(&msg)?; + let inbound_msg = DlcMessage::new(node_id, serialized_inbound_message, true)?; + match db::dlc_messages::DlcMessage::get(&mut conn, inbound_msg.message_hash)? { + Some(_) => { tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); return Ok(()); } + None => inbound_msg, } }; @@ -230,181 +198,103 @@ impl Node { ) })?; } + ChannelMessage::RenewOffer(r) => { + tracing::info!("Automatically accepting a rollover position"); + let (accept_renew_offer, counterparty_pubkey) = + self.inner.dlc_manager.accept_renew_offer(&r.channel_id)?; + + self.send_dlc_message( + counterparty_pubkey, + Message::Channel(ChannelMessage::RenewAccept(accept_renew_offer)), + )?; + + let expiry_timestamp = OffsetDateTime::from_unix_timestamp( + r.contract_info.get_closest_maturity_date() as i64, + )?; + position::handler::rollover_position(expiry_timestamp)?; + } + ChannelMessage::RenewRevoke(_) => { + tracing::info!("Finished rollover position"); + // After handling the `RenewRevoke` message, we need to do some + // post-processing based on the fact that the DLC + // channel has been updated. + position::handler::set_position_state(PositionState::Open)?; + + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::Rollover(TaskStatus::Success), + )); + } + // ignoring all other channel events. + ChannelMessage::Sign(signed) => { + let (accept_collateral, expiry_timestamp) = self + .inner + .get_collateral_and_expiry_for_confirmed_dlc_channel( + signed.channel_id, + )?; + + let filled_order = order::handler::order_filled() + .context("Cannot mark order as filled for confirmed DLC")?; + + let execution_price = filled_order + .execution_price() + .expect("filled order to have a price"); + let open_position_fee = order_matching_fee_taker( + filled_order.quantity, + Decimal::try_from(execution_price)?, + ); + + position::handler::update_position_after_dlc_creation( + filled_order, + accept_collateral - open_position_fee.to_sat(), + expiry_timestamp, + ) + .context("Failed to update position after DLC creation")?; + + // Sending always a recover dlc background notification success message here + // as we do not know if we might have reached this + // state after a restart. This event is only + // received by the UI at the moment indicating that + // the dialog can be closed. If the dialog is not + // open, this event would be simply ignored by the UI. + // + // FIXME(holzeis): We should not require that event and align the UI + // handling with waiting for an order execution in + // the happy case with waiting for an order + // execution after an in between restart. For now it + // was the easiest to go parallel to + // that implementation so that we don't have to touch it. + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Success), + )); + } _ => (), } { let mut conn = db::connection()?; - dlc_message_step.finish(&mut conn, &resp)?; + db::dlc_messages::DlcMessage::insert(&mut conn, inbound_msg)?; } - resp - } - Message::SubChannel(ref msg) => { - let resp = self - .inner - .sub_channel_manager - .on_sub_channel_message(msg, &node_id) - .with_context(|| { - format!( - "Failed to handle {} message from {node_id}", - sub_channel_message_name(msg) - ) - })? - .map(Message::SubChannel); - - // Some incoming messages require extra action from our part for the protocol to - // continue - match msg { - SubChannelMessage::Offer(offer) => { - // TODO: We should probably verify that: (1) the counterparty is the - // coordinator and (2) the DLC channel offer is expected and correct. - let action = decide_subchannel_offer_action( - OffsetDateTime::from_unix_timestamp( - offer.contract_info.get_closest_maturity_date() as i64, - ) - .expect("A contract should always have a valid maturity timestamp."), - ); - self.process_subchannel_offer(offer.channel_id, action)?; - } - SubChannelMessage::CloseOffer(offer) => { - let channel_id = offer.channel_id; - - // TODO: We should probably verify that: (1) the counterparty is the - // coordinator and (2) the DLC channel close offer is expected and correct. - self.inner - .accept_sub_channel_collaborative_settlement(&channel_id) - .with_context(|| { - format!( - "Failed to accept sub channel close offer for channel {}", - hex::encode(channel_id.0) - ) - })?; - } - _ => (), - }; - resp } }; - // TODO(holzeis): It would be nice if dlc messages are also propagated via events, so the - // receiver can decide what events to process and we can skip this component specific logic - // here. - if let Message::Channel(channel_message) = &msg { - match channel_message { - ChannelMessage::RenewOffer(r) => { - tracing::info!("Automatically accepting a rollover position"); - let (accept_renew_offer, counterparty_pubkey) = - self.inner.dlc_manager.accept_renew_offer(&r.channel_id)?; - - self.send_dlc_message( - counterparty_pubkey, - Message::Channel(ChannelMessage::RenewAccept(accept_renew_offer)), - )?; - - let expiry_timestamp = OffsetDateTime::from_unix_timestamp( - r.contract_info.get_closest_maturity_date() as i64, - )?; - position::handler::rollover_position(expiry_timestamp)?; - } - ChannelMessage::RenewRevoke(_) => { - tracing::info!("Finished rollover position"); - // After handling the `RenewRevoke` message, we need to do some post-processing - // based on the fact that the DLC channel has been updated. - position::handler::set_position_state(PositionState::Open)?; - - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::Rollover(TaskStatus::Success), - )); - } - // ignoring all other channel events. - ChannelMessage::Sign(signed) => { - let (accept_collateral, expiry_timestamp) = self - .inner - .get_collateral_and_expiry_for_confirmed_dlc_channel(signed.channel_id)?; - - let filled_order = order::handler::order_filled() - .context("Cannot mark order as filled for confirmed DLC")?; - - let execution_price = filled_order - .execution_price() - .expect("filled order to have a price"); - let open_position_fee = order_matching_fee_taker( - filled_order.quantity, - Decimal::try_from(execution_price)?, - ); + if let Some(msg) = resp { + self.send_dlc_message(node_id, msg.clone())?; - position::handler::update_position_after_dlc_creation( - filled_order, - accept_collateral - open_position_fee.to_sat(), - expiry_timestamp, - ) - .context("Failed to update position after DLC creation")?; - - // Sending always a recover dlc background notification success message here as - // we do not know if we might have reached this state after - // a restart. This event is only received by the UI at the - // moment indicating that the dialog can be closed. - // If the dialog is not open, this event would be simply ignored by the UI. - // - // FIXME(holzeis): We should not require that event and align the UI handling - // with waiting for an order execution in the happy case - // with waiting for an order execution after an in between - // restart. For now it was the easiest to go parallel to - // that implementation so that we don't have to touch it. - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::RecoverDlc(TaskStatus::Success), - )); - } - _ => {} - } - } + if let Message::Channel(ChannelMessage::SettleFinalize(_)) = msg { + tracing::debug!("Position based on DLC channel is being closed"); - // After handling the `Revoke` message, we need to do some post-processing based on the fact - // that the DLC channel has been established - if let Message::SubChannel(SubChannelMessage::Revoke(SubChannelRevoke { - channel_id, .. - })) = msg - { - let (accept_collateral, expiry_timestamp) = self - .inner - .get_collateral_and_expiry_for_confirmed_contract(channel_id)?; - - let filled_order = order::handler::order_filled() - .context("Cannot mark order as filled for confirmed DLC")?; - - let execution_price = filled_order - .execution_price() - .expect("filled order to have a price"); - let open_position_fee = order_matching_fee_taker( - filled_order.quantity, - Decimal::try_from(execution_price)?, - ); + let filled_order = order::handler::order_filled()?; - position::handler::update_position_after_dlc_creation( - filled_order, - accept_collateral - open_position_fee.to_sat(), - expiry_timestamp, - ) - .context("Failed to update position after DLC creation")?; - - // Sending always a recover dlc background notification success message here as we do - // not know if we might have reached this state after a restart. This event is only - // received by the UI at the moment indicating that the dialog can be closed. - // If the dialog is not open, this event would be simply ignored by the UI. - // - // FIXME(holzeis): We should not require that event and align the UI handling with - // waiting for an order execution in the happy case with waiting for an - // order execution after an in between restart. For now it was the easiest - // to go parallel to that implementation so that we don't have to touch it. - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::RecoverDlc(TaskStatus::Success), - )); - } + position::handler::update_position_after_dlc_closure(Some(filled_order)) + .context("Failed to update position after DLC closure")?; - if let Some(msg) = resp { - self.send_dlc_message(node_id, msg)?; + // In case of a restart. + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Success), + )); + } } Ok(()) @@ -534,62 +424,11 @@ impl Node { ); self.inner - .dlc_message_handler - .send_message(node_id, msg.clone()); - - if let Message::Channel(ChannelMessage::SettleFinalize(_)) = msg { - tracing::debug!("Position based on DLC channel is being closed"); - - let filled_order = order::handler::order_filled()?; - - position::handler::update_position_after_dlc_closure(Some(filled_order)) - .context("Failed to update position after DLC closure")?; - - // In case of a restart. - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::RecoverDlc(TaskStatus::Success), - )); - } - - // After sending the `CloseFinalize` message, we need to do some post-processing based on - // the fact that the DLC channel has been closed. - if let Message::SubChannel(SubChannelMessage::CloseFinalize(SubChannelCloseFinalize { - .. - })) = msg - { - tracing::debug!( - "Checking purpose of sending SubChannelCloseFinalize w.r.t. the position" - ); - - let positions = position::handler::get_positions()?; - let position = positions - .first() - .context("Cannot find position even though we just received a SubChannelMessage")?; - - if position.position_state == PositionState::Resizing { - tracing::debug!("Position is being resized"); - } else { - tracing::debug!("Position is being closed"); - - let filled_order = order::handler::order_filled()?; - - position::handler::update_position_after_dlc_closure(Some(filled_order)) - .context("Failed to update position after DLC closure")?; - } - - // Sending always a recover dlc background notification success message here as we do - // not know if we might have reached this state after a restart. This event is only - // received by the UI at the moment indicating that the dialog can be closed. - // If the dialog is not open, this event would be simply ignored by the UI. - // - // FIXME(holzeis): We should not require that event and align the UI handling with - // waiting for an order execution in the happy case with waiting for an - // order execution after an in between restart. For now it was the easiest - // to go parallel to that implementation so that we don't have to touch it. - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::RecoverDlc(TaskStatus::Success), - )); - }; + .event_handler + .publish(NodeEvent::SendDlcMessage { + peer: node_id, + msg: msg.clone(), + })?; Ok(()) } diff --git a/mobile/native/src/schema.rs b/mobile/native/src/schema.rs index ce6145bee..8dcd9856c 100644 --- a/mobile/native/src/schema.rs +++ b/mobile/native/src/schema.rs @@ -23,7 +23,6 @@ diesel::table! { inbound -> Bool, peer_id -> Text, message_type -> Text, - message_sub_type -> Text, timestamp -> BigInt, } } From 14a2169200d352a5c842b059310550bfff3cc319 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 16 Jan 2024 10:30:30 +0100 Subject: [PATCH 8/8] chore: Post process position state on inbound dlc message Instead of processing the position state on the outbound dlc message. IMHO the logic is easier to understand that way and the code is more cohesive that way. (at least all the code that needs to be refactored is now in one place :) --- .../2024-01-10-105231_add_dlc_messages/up.sql | 1 - coordinator/src/bin/coordinator.rs | 5 -- coordinator/src/db/dlc_messages.rs | 12 ++--- .../src/db/last_outbound_dlc_message.rs | 2 +- coordinator/src/dlc_handler.rs | 5 ++ coordinator/src/node.rs | 46 ++++++++++++------- crates/ln-dlc-node/src/dlc_message.rs | 17 +++---- crates/ln-dlc-node/src/tests/mod.rs | 2 +- .../2024-01-10-105231_add_dlc_messages/up.sql | 1 - mobile/native/src/db/dlc_messages.rs | 8 ++-- mobile/native/src/ln_dlc/node.rs | 46 ++++++++++++------- 11 files changed, 85 insertions(+), 60 deletions(-) diff --git a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql index 832f0dc57..73fc59d27 100644 --- a/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql +++ b/coordinator/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -16,7 +16,6 @@ CREATE TYPE "Message_Type_Type" AS ENUM ( ); CREATE TABLE "dlc_messages" ( - -- We need to store the hash as TEXT as the BIGINT type overflows on some u64 values breaking the hash value. message_hash TEXT PRIMARY KEY NOT NULL, inbound BOOLEAN NOT NULL, peer_id TEXT NOT NULL, diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index b048a77f0..8890b798a 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -144,11 +144,6 @@ async fn main() -> Result<()> { let dlc_handler = DlcHandler::new(pool.clone(), node.clone()); let _handle = - // this handles sending outbound dlc messages as well as keeping track of what - // dlc messages have already been processed and what was the last outbound dlc message - // so it can be resend on reconnect. - // - // this does not handle the incoming dlc messages! dlc_handler::spawn_handling_dlc_messages(dlc_handler, node_event_handler.subscribe()); let event_handler = CoordinatorEventHandler::new(node.clone(), Some(node_event_sender)); diff --git a/coordinator/src/db/dlc_messages.rs b/coordinator/src/db/dlc_messages.rs index 4e4a94425..cac26fc2d 100644 --- a/coordinator/src/db/dlc_messages.rs +++ b/coordinator/src/db/dlc_messages.rs @@ -59,7 +59,7 @@ pub(crate) struct DlcMessage { pub timestamp: OffsetDateTime, } -pub(crate) fn get(conn: &mut PgConnection, message_hash: u64) -> QueryResult> { +pub(crate) fn get(conn: &mut PgConnection, message_hash: &str) -> QueryResult> { dlc_messages::table .filter(dlc_messages::message_hash.eq(message_hash.to_string())) .first::(conn) @@ -82,9 +82,9 @@ pub(crate) fn insert( impl From for DlcMessage { fn from(value: ln_dlc_node::dlc_message::DlcMessage) -> Self { Self { - message_hash: value.message_hash.to_string(), + message_hash: value.message_hash, peer_id: value.peer_id.to_string(), - message_type: MessageType::from(value.clone().message_type), + message_type: MessageType::from(value.message_type), timestamp: value.timestamp, inbound: value.inbound, } @@ -117,11 +117,9 @@ impl From for MessageType { impl From for ln_dlc_node::dlc_message::DlcMessage { fn from(value: DlcMessage) -> Self { Self { - message_hash: u64::from_str(&value.message_hash).expect("valid u64"), + message_hash: value.message_hash, inbound: value.inbound, - message_type: ln_dlc_node::dlc_message::DlcMessageType::from( - value.clone().message_type, - ), + message_type: ln_dlc_node::dlc_message::DlcMessageType::from(value.message_type), peer_id: PublicKey::from_str(&value.peer_id).expect("valid public key"), timestamp: value.timestamp, } diff --git a/coordinator/src/db/last_outbound_dlc_message.rs b/coordinator/src/db/last_outbound_dlc_message.rs index 92d82fa36..3ffabedb9 100644 --- a/coordinator/src/db/last_outbound_dlc_message.rs +++ b/coordinator/src/db/last_outbound_dlc_message.rs @@ -61,7 +61,7 @@ pub(crate) fn upsert( ) -> Result<()> { let values = ( last_outbound_dlc_messages::peer_id.eq(peer_id.to_string()), - last_outbound_dlc_messages::message_hash.eq(sdm.generate_hash().to_string()), + last_outbound_dlc_messages::message_hash.eq(sdm.generate_hash()), last_outbound_dlc_messages::message.eq(sdm.message), ); let affected_rows = diesel::insert_into(last_outbound_dlc_messages::table) diff --git a/coordinator/src/dlc_handler.rs b/coordinator/src/dlc_handler.rs index 265a3e7d8..25b175d02 100644 --- a/coordinator/src/dlc_handler.rs +++ b/coordinator/src/dlc_handler.rs @@ -40,6 +40,11 @@ impl DlcHandler { } } +/// [`spawn_handling_dlc_messages`] handles sending outbound dlc messages as well as keeping track +/// of what dlc messages have already been processed and what was the last outbound dlc message +/// so it can be resend on reconnect. +/// +/// It does not handle the incoming dlc messages! pub fn spawn_handling_dlc_messages( dlc_handler: DlcHandler, mut receiver: broadcast::Receiver, diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 72218967b..1ff7006d0 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -591,6 +591,21 @@ impl Node { } } + /// [`process_dlc_message`] processes incoming dlc channel messages and updates the 10101 + /// position accordingly. + /// - Any other message will be ignored. + /// - Any dlc channel message that has already been processed will be skipped. + /// + /// If an offer is received [`ChannelMessage::Offer`], [`ChannelMessage::SettleOffer`], + /// [`ChannelMessage::CollaborativeCloseOffer`] [`ChannelMessage::RenewOffer`] will get + /// automatically accepted. Unless the maturity date of the offer is already outdated. + /// + /// FIXME(holzeis): This function manipulates different data objects in different data sources + /// and should use a transaction to make all changes atomic. Not doing so risks of ending up in + /// an inconsistent state. One way of fixing that could be to + /// (1) use a single data source for the 10101 data and the rust-dlc data. + /// (2) wrap the function into a db transaction which can be atomically rolled back on error or + /// committed on success. fn process_dlc_message(&self, node_id: PublicKey, msg: Message) -> Result<()> { tracing::info!( from = %node_id, @@ -608,7 +623,7 @@ impl Node { let mut conn = self.pool.get()?; let serialized_inbound_message = SerializedDlcMessage::try_from(&msg)?; let inbound_msg = DlcMessage::new(node_id, serialized_inbound_message, true)?; - match db::dlc_messages::get(&mut conn, inbound_msg.message_hash)? { + match db::dlc_messages::get(&mut conn, &inbound_msg.message_hash)? { Some(_) => { tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); return Ok(()); @@ -675,6 +690,20 @@ impl Node { self.inner .accept_dlc_channel_collaborative_close(close_offer.channel_id)?; } + ChannelMessage::Accept(accept_channel) => { + let channel_id_hex_string = accept_channel.temporary_channel_id.to_hex(); + tracing::info!( + channel_id = channel_id_hex_string, + node_id = node_id.to_string(), + "DLC channel open protocol was accepted" + ); + let mut connection = self.pool.get()?; + db::positions::Position::update_proposed_position( + &mut connection, + node_id.to_string(), + PositionState::Open, + )?; + } _ => {} }; @@ -682,21 +711,6 @@ impl Node { } }; - if let Some(Message::Channel(ChannelMessage::Sign(sign_channel))) = &resp { - let channel_id_hex_string = sign_channel.channel_id.to_hex(); - tracing::info!( - channel_id = channel_id_hex_string, - node_id = node_id.to_string(), - "DLC channel open protocol was finalized" - ); - let mut connection = self.pool.get()?; - db::positions::Position::update_proposed_position( - &mut connection, - node_id.to_string(), - PositionState::Open, - )?; - } - if let Some(msg) = resp { tracing::info!( to = %node_id, diff --git a/crates/ln-dlc-node/src/dlc_message.rs b/crates/ln-dlc-node/src/dlc_message.rs index 9d6eea78e..84e625190 100644 --- a/crates/ln-dlc-node/src/dlc_message.rs +++ b/crates/ln-dlc-node/src/dlc_message.rs @@ -1,16 +1,17 @@ use anyhow::Result; +use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; use dlc_messages::ChannelMessage; use dlc_messages::Message; -use std::collections::hash_map::DefaultHasher; -use std::hash::Hash; -use std::hash::Hasher; +use sha2::digest::FixedOutput; +use sha2::Digest; +use sha2::Sha256; use time::OffsetDateTime; use ureq::serde_json; #[derive(Clone)] pub struct DlcMessage { - pub message_hash: u64, + pub message_hash: String, pub inbound: bool, pub peer_id: PublicKey, pub message_type: DlcMessageType, @@ -42,10 +43,10 @@ pub struct SerializedDlcMessage { } impl SerializedDlcMessage { - pub fn generate_hash(&self) -> u64 { - let mut hasher = DefaultHasher::new(); - self.hash(&mut hasher); - hasher.finish() + pub fn generate_hash(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(self.message.as_bytes()); + hasher.finalize_fixed().to_hex() } } diff --git a/crates/ln-dlc-node/src/tests/mod.rs b/crates/ln-dlc-node/src/tests/mod.rs index f9ee07892..3e3e8c1d3 100644 --- a/crates/ln-dlc-node/src/tests/mod.rs +++ b/crates/ln-dlc-node/src/tests/mod.rs @@ -247,7 +247,7 @@ impl Node { Ok(NodeEvent::Connected { .. }) => {} // ignored Err(_) => { tracing::error!( - "Failed to receive message from node even handler channel." + "Failed to receive message from node event handler channel." ); break; } diff --git a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql index 38ffd35e2..cbbdf9b33 100644 --- a/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql +++ b/mobile/native/migrations/2024-01-10-105231_add_dlc_messages/up.sql @@ -1,5 +1,4 @@ CREATE TABLE "dlc_messages" ( - -- We need to store the hash as TEXT as the BIGINT type overflows on some u64 values breaking the hash value. message_hash TEXT PRIMARY KEY NOT NULL, inbound BOOLEAN NOT NULL, peer_id TEXT NOT NULL, diff --git a/mobile/native/src/db/dlc_messages.rs b/mobile/native/src/db/dlc_messages.rs index ba3bd75cc..bdcbd0a6f 100644 --- a/mobile/native/src/db/dlc_messages.rs +++ b/mobile/native/src/db/dlc_messages.rs @@ -50,7 +50,7 @@ pub enum MessageType { impl DlcMessage { pub(crate) fn get( conn: &mut SqliteConnection, - message_hash: u64, + message_hash: &str, ) -> QueryResult> { let result = schema::dlc_messages::table .filter(schema::dlc_messages::message_hash.eq(message_hash.to_string())) @@ -77,9 +77,9 @@ impl DlcMessage { impl From for DlcMessage { fn from(value: ln_dlc_node::dlc_message::DlcMessage) -> Self { Self { - message_hash: value.message_hash.to_string(), + message_hash: value.clone().message_hash, peer_id: value.peer_id.to_string(), - message_type: MessageType::from(value.clone().message_type), + message_type: MessageType::from(value.message_type), timestamp: value.timestamp.unix_timestamp(), inbound: value.inbound, } @@ -115,7 +115,7 @@ impl From for ln_dlc_node::dlc_message::DlcMessage { ln_dlc_node::dlc_message::DlcMessageType::from(value.clone().message_type); Self { - message_hash: u64::from_str(&value.message_hash).expect("valid u64"), + message_hash: value.message_hash, inbound: value.inbound, message_type: dlc_message_type, peer_id: PublicKey::from_str(&value.peer_id).expect("valid public key"), diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index 77b9721a3..bc0529cb0 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -141,6 +141,21 @@ impl Node { } } + /// [`process_dlc_message`] processes incoming dlc channel messages and updates the 10101 + /// position accordingly. + /// - Any other message will be ignored. + /// - Any dlc channel message that has already been processed will be skipped. + /// + /// If an offer is received [`ChannelMessage::Offer`], [`ChannelMessage::SettleOffer`], + /// [`ChannelMessage::RenewOffer`] will get automatically accepted. Unless the maturity date of + /// the offer is already outdated. + /// + /// FIXME(holzeis): This function manipulates different data objects in different data sources + /// and should use a transaction to make all changes atomic. Not doing so risks of ending up in + /// an inconsistent state. One way of fixing that could be to + /// (1) use a single data source for the 10101 data and the rust-dlc data. + /// (2) wrap the function into a db transaction which can be atomically rolled back on error or + /// committed on success. fn process_dlc_message(&self, node_id: PublicKey, msg: Message) -> Result<()> { tracing::info!( from = %node_id, @@ -152,13 +167,13 @@ impl Node { Message::OnChain(_) | Message::SubChannel(_) => { tracing::warn!("Ignoring unexpected dlc message."); None - }, + } Message::Channel(channel_msg) => { let inbound_msg = { let mut conn = db::connection()?; let serialized_inbound_message = SerializedDlcMessage::try_from(&msg)?; let inbound_msg = DlcMessage::new(node_id, serialized_inbound_message, true)?; - match db::dlc_messages::DlcMessage::get(&mut conn, inbound_msg.message_hash)? { + match db::dlc_messages::DlcMessage::get(&mut conn, &inbound_msg.message_hash)? { Some(_) => { tracing::debug!(%node_id, kind=%dlc_message_name(&msg), "Received message that has already been processed, skipping."); return Ok(()); @@ -267,6 +282,19 @@ impl Node { BackgroundTask::RecoverDlc(TaskStatus::Success), )); } + ChannelMessage::SettleConfirm(_) => { + tracing::debug!("Position based on DLC channel is being closed"); + + let filled_order = order::handler::order_filled()?; + + position::handler::update_position_after_dlc_closure(Some(filled_order)) + .context("Failed to update position after DLC closure")?; + + // In case of a restart. + event::publish(&EventInternal::BackgroundNotification( + BackgroundTask::RecoverDlc(TaskStatus::Success), + )); + } _ => (), } @@ -281,20 +309,6 @@ impl Node { if let Some(msg) = resp { self.send_dlc_message(node_id, msg.clone())?; - - if let Message::Channel(ChannelMessage::SettleFinalize(_)) = msg { - tracing::debug!("Position based on DLC channel is being closed"); - - let filled_order = order::handler::order_filled()?; - - position::handler::update_position_after_dlc_closure(Some(filled_order)) - .context("Failed to update position after DLC closure")?; - - // In case of a restart. - event::publish(&EventInternal::BackgroundNotification( - BackgroundTask::RecoverDlc(TaskStatus::Success), - )); - } } Ok(())