From 24a27f66078c5345a0b48b50020a3771713347f5 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 31 May 2024 13:51:36 +0200 Subject: [PATCH 1/4] fix: Use r_hash from subscribed invoice params --- Cargo.lock | 1 - coordinator/src/node/invoice.rs | 28 +++++++++++++++------------- mobile/native/Cargo.toml | 1 - mobile/native/src/watcher.rs | 18 +++--------------- 4 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a98d483..5948d67f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2569,7 +2569,6 @@ version = "2.4.5" dependencies = [ "aes-gcm-siv", "anyhow", - "base64 0.21.7", "bdk", "bdk_file_store", "bip21", diff --git a/coordinator/src/node/invoice.rs b/coordinator/src/node/invoice.rs index b43672f4a..04bf8a99a 100644 --- a/coordinator/src/node/invoice.rs +++ b/coordinator/src/node/invoice.rs @@ -28,22 +28,24 @@ pub fn spawn_invoice_watch( match stream.try_next().await { Ok(Some(invoice)) => match invoice.state { InvoiceState::Open => { - tracing::debug!(%trader_pubkey, invoice.r_hash, "Watching hodl invoice."); + tracing::debug!(%trader_pubkey, r_hash, "Watching hodl invoice."); continue; } InvoiceState::Settled => { - tracing::info!(%trader_pubkey, invoice.r_hash, "Accepted hodl invoice has been settled."); + tracing::info!(%trader_pubkey, r_hash, "Accepted hodl invoice has been settled."); break; } InvoiceState::Canceled => { - tracing::warn!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been canceled."); - if let Err(e) = spawn_blocking(move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed_by_r_hash( - &mut conn, - invoice.r_hash, - )?; - anyhow::Ok(()) + tracing::warn!(%trader_pubkey, r_hash, "Pending hodl invoice has been canceled."); + if let Err(e) = spawn_blocking({ + let r_hash = r_hash.clone(); + move || { + let mut conn = pool.get()?; + db::hodl_invoice::update_hodl_invoice_to_failed_by_r_hash( + &mut conn, r_hash, + )?; + anyhow::Ok(()) + } }) .await .expect("task to finish") @@ -56,12 +58,12 @@ pub fn spawn_invoice_watch( break; } InvoiceState::Accepted => { - tracing::info!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been accepted."); + tracing::info!(%trader_pubkey, r_hash, "Pending hodl invoice has been accepted."); if let Err(e) = trader_sender.send(Message::LnPaymentReceived { - r_hash: invoice.r_hash.clone(), + r_hash: r_hash.clone(), amount: Amount::from_sat(invoice.amt_paid_sat), }) { - tracing::error!(%trader_pubkey, r_hash = invoice.r_hash, "Failed to send payment received event to app. Error: {e:#}") + tracing::error!(%trader_pubkey, r_hash, "Failed to send payment received event to app. Error: {e:#}") } continue; } diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml index cbbcc6532..84300ed4a 100644 --- a/mobile/native/Cargo.toml +++ b/mobile/native/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["rlib", "cdylib", "staticlib"] [dependencies] aes-gcm-siv = { version = "0.11.1", features = ["heapless"] } anyhow = "1" -base64 = "0.21.0" bdk = { version = "1.0.0-alpha.6", features = ["std"] } bdk_file_store = "0.6" bip21 = "0.3.0" diff --git a/mobile/native/src/watcher.rs b/mobile/native/src/watcher.rs index 268a9a66d..c6bea08e6 100644 --- a/mobile/native/src/watcher.rs +++ b/mobile/native/src/watcher.rs @@ -3,8 +3,6 @@ use crate::event::EventInternal; use crate::event::EventType; use crate::state; use anyhow::Result; -use base64::engine::general_purpose; -use base64::Engine; use bitcoin::Address; use bitcoin::Amount; use std::time::Duration; @@ -33,19 +31,9 @@ impl Subscriber for InvoiceWatcher { let r_hash = r_hash.clone(); let sender = self.sender.clone(); async move { - // We receive the r_hash in base64 standard encoding - match general_purpose::STANDARD.decode(&r_hash) { - Ok(hash) => { - // but we watch for the r_has in base64 url safe encoding. - let r_hash = general_purpose::URL_SAFE.encode(hash); - if let Err(e) = sender.send(r_hash.clone()) { - tracing::error!(%r_hash, "Failed to send accepted invoice event. Error: {e:#}"); - } - }, - Err(e) => { - tracing::error!(r_hash, "Failed to decode. Error: {e:#}"); - } - }; + if let Err(e) = sender.send(r_hash.clone()) { + tracing::error!(%r_hash, "Failed to send accepted invoice event. Error: {e:#}"); + } } }); } From 73164d9655d5db4ff9a16505ed24bb12aefa5c97 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 31 May 2024 14:46:23 +0200 Subject: [PATCH 2/4] feat: Cancel hodl invoice if dlc proposal fails --- coordinator/src/db/hodl_invoice.rs | 21 ++---- coordinator/src/trade/mod.rs | 73 ++++++++----------- .../lnd-bridge/examples/cancel_invoice_api.rs | 4 +- crates/lnd-bridge/src/lib.rs | 6 +- 4 files changed, 43 insertions(+), 61 deletions(-) diff --git a/coordinator/src/db/hodl_invoice.rs b/coordinator/src/db/hodl_invoice.rs index ad06a7f82..0e44de855 100644 --- a/coordinator/src/db/hodl_invoice.rs +++ b/coordinator/src/db/hodl_invoice.rs @@ -9,6 +9,7 @@ use diesel::AsExpression; use diesel::ExpressionMethods; use diesel::FromSqlRow; use diesel::PgConnection; +use diesel::QueryDsl; use diesel::QueryResult; use diesel::RunQueryDsl; use std::any::TypeId; @@ -53,6 +54,13 @@ pub fn create_hodl_invoice( Ok(()) } +pub fn get_r_hash_by_order_id(conn: &mut PgConnection, order_id: Uuid) -> QueryResult { + hodl_invoices::table + .filter(hodl_invoices::order_id.eq(order_id)) + .select(hodl_invoices::r_hash) + .get_result(conn) +} + pub fn update_hodl_invoice_to_accepted( conn: &mut PgConnection, hash: &str, @@ -87,19 +95,6 @@ pub fn update_hodl_invoice_to_settled( .get_result(conn) } -pub fn update_hodl_invoice_to_failed( - conn: &mut PgConnection, - order_id: Uuid, -) -> QueryResult { - diesel::update(hodl_invoices::table) - .filter(hodl_invoices::order_id.eq(order_id)) - .set(( - hodl_invoices::updated_at.eq(OffsetDateTime::now_utc()), - hodl_invoices::invoice_state.eq(InvoiceState::Failed), - )) - .execute(conn) -} - pub fn update_hodl_invoice_to_failed_by_r_hash( conn: &mut PgConnection, r_hash: String, diff --git a/coordinator/src/trade/mod.rs b/coordinator/src/trade/mod.rs index bb01fe525..a897ee057 100644 --- a/coordinator/src/trade/mod.rs +++ b/coordinator/src/trade/mod.rs @@ -149,7 +149,17 @@ impl TradeExecutor { if params.external_funding.is_some() { // The channel was funded externally. We need to post process the dlc channel // offer. - if let Err(e) = self.post_process_proposal(trader_id, order_id).await { + if let Err(e) = self.settle_invoice(trader_id, order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to settle invoice with provided pre_image. Cancelling offer. Error: {e:#}"); + + if let Err(e) = self.cancel_offer(trader_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel offer. Error: {e:#}"); + } + + if let Err(e) = self.cancel_hodl_invoice(order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel hodl invoice. Error: {e:#}"); + } + let message = OrderbookMessage::TraderMessage { trader_id, message: Message::TradeError { @@ -183,20 +193,8 @@ impl TradeExecutor { tracing::error!(%trader_id, %order_id, "Failed to cancel offer. Error: {e:#}"); } - // if the order was externally funded we need to set the hodl invoice to failed. - if let Err(e) = spawn_blocking({ - let pool = self.node.pool.clone(); - move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed(&mut conn, order_id)?; - - anyhow::Ok(()) - } - }) - .await - .expect("task to finish") - { - tracing::error!(%trader_id, %order_id, "Failed to set hodl invoice to failed. Error: {e:#}"); + if let Err(e) = self.cancel_hodl_invoice(order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel hodl_invoice. Error: {e:#}"); } } @@ -221,35 +219,6 @@ impl TradeExecutor { }; } - async fn post_process_proposal(&self, trader: PublicKey, order_id: Uuid) -> Result<()> { - match self.settle_invoice(trader, order_id).await { - Ok(()) => Ok(()), - Err(e) => { - tracing::error!(%trader, %order_id, "Failed to settle invoice with provided pre_image. Cancelling offer. Error: {e:#}"); - - if let Err(e) = self.cancel_offer(trader).await { - tracing::error!(%trader, %order_id, "Failed to cancel offer. Error: {e:#}"); - } - - if let Err(e) = spawn_blocking({ - let pool = self.node.pool.clone(); - move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed(&mut conn, order_id)?; - - anyhow::Ok(()) - } - }) - .await - .expect("task to finish") - { - tracing::error!(%trader, %order_id, "Failed to set hodl invoice to failed. Error: {e:#}"); - } - Err(e) - } - } - } - /// Settles the accepted invoice for the given trader async fn settle_invoice(&self, trader: PublicKey, order_id: Uuid) -> Result<()> { let pre_image = spawn_blocking({ @@ -305,6 +274,22 @@ impl TradeExecutor { Ok(()) } + pub async fn cancel_hodl_invoice(&self, order_id: Uuid) -> Result<()> { + // if the order was externally funded we need to set the hodl invoice to failed. + let r_hash = spawn_blocking({ + let pool = self.node.pool.clone(); + move || { + let mut conn = pool.get()?; + let r_hash = db::hodl_invoice::get_r_hash_by_order_id(&mut conn, order_id)?; + + anyhow::Ok(r_hash) + } + }) + .await??; + + self.node.lnd_bridge.cancel_invoice(r_hash).await + } + /// Execute a trade action according to the coordinator's current trading status with the /// trader. /// diff --git a/crates/lnd-bridge/examples/cancel_invoice_api.rs b/crates/lnd-bridge/examples/cancel_invoice_api.rs index 9395d1b55..860ff9b52 100644 --- a/crates/lnd-bridge/examples/cancel_invoice_api.rs +++ b/crates/lnd-bridge/examples/cancel_invoice_api.rs @@ -9,8 +9,8 @@ async fn main() -> Result<()> { let macaroon = "[enter macroon here]".to_string(); let lnd_bridge = lnd_bridge::LndBridge::new("localhost:18080".to_string(), macaroon, false); - let payment_hash = "".to_string(); - lnd_bridge.cancel_invoice(payment_hash).await?; + let r_hash = "".to_string(); + lnd_bridge.cancel_invoice(r_hash).await?; Ok(()) } diff --git a/crates/lnd-bridge/src/lib.rs b/crates/lnd-bridge/src/lib.rs index 3f67a40a9..089ee9799 100644 --- a/crates/lnd-bridge/src/lib.rs +++ b/crates/lnd-bridge/src/lib.rs @@ -150,7 +150,7 @@ impl LndBridge { Ok(invoice) } - pub async fn cancel_invoice(&self, payment_hash: String) -> Result<()> { + pub async fn cancel_invoice(&self, r_hash: String) -> Result<()> { let builder = self.client.request( Method::POST, format!( @@ -163,7 +163,9 @@ impl LndBridge { let resp = builder .header("content-type", "application/json") .header("Grpc-Metadata-macaroon", self.macaroon.clone()) - .json(&CancelInvoice { payment_hash }) + .json(&CancelInvoice { + payment_hash: r_hash, + }) .send() .await?; From f72e1f08e0f78b12d528f00bb50e79fda8751590 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 31 May 2024 14:52:37 +0200 Subject: [PATCH 3/4] feat: Set invoice state to canceled instead of failed We can't really know if the invoice failed or got canceled. I think canceled is the better state name though. --- .../2024-05-31-144618_add-canceled-invoice-state/down.sql | 5 +++++ .../2024-05-31-144618_add-canceled-invoice-state/up.sql | 1 + coordinator/src/db/custom_types.rs | 2 ++ coordinator/src/db/hodl_invoice.rs | 5 +++-- coordinator/src/node/invoice.rs | 2 +- 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql create mode 100644 coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql diff --git a/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql new file mode 100644 index 000000000..79aacce09 --- /dev/null +++ b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql @@ -0,0 +1,5 @@ +-- Postgres does not allow removing enum type values. One can only re-create an enum type with fewer values and replace the references. +-- However, there is no proper way to replace the values to be removed where they are used (i.e. referenced in `hodl_invoice` table) +-- We opt to NOT remove enum values that were added at a later point. + +select 1; diff --git a/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql new file mode 100644 index 000000000..d0124bf86 --- /dev/null +++ b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql @@ -0,0 +1 @@ +ALTER TYPE "InvoiceState_Type" ADD VALUE IF NOT EXISTS 'Canceled'; \ No newline at end of file diff --git a/coordinator/src/db/custom_types.rs b/coordinator/src/db/custom_types.rs index 633dbffc2..57b91d3a4 100644 --- a/coordinator/src/db/custom_types.rs +++ b/coordinator/src/db/custom_types.rs @@ -275,6 +275,7 @@ impl ToSql for InvoiceState { InvoiceState::Accepted => out.write_all(b"Accepted")?, InvoiceState::Settled => out.write_all(b"Settled")?, InvoiceState::Failed => out.write_all(b"Failed")?, + InvoiceState::Canceled => out.write_all(b"Canceled")?, } Ok(IsNull::No) } @@ -287,6 +288,7 @@ impl FromSql for InvoiceState { b"Accepted" => Ok(InvoiceState::Accepted), b"Settled" => Ok(InvoiceState::Settled), b"Failed" => Ok(InvoiceState::Failed), + b"Canceled" => Ok(InvoiceState::Canceled), _ => Err("Unrecognized enum variant".into()), } } diff --git a/coordinator/src/db/hodl_invoice.rs b/coordinator/src/db/hodl_invoice.rs index 0e44de855..033503cae 100644 --- a/coordinator/src/db/hodl_invoice.rs +++ b/coordinator/src/db/hodl_invoice.rs @@ -23,6 +23,7 @@ pub enum InvoiceState { Accepted, Settled, Failed, + Canceled, } impl QueryId for InvoiceStateType { @@ -95,7 +96,7 @@ pub fn update_hodl_invoice_to_settled( .get_result(conn) } -pub fn update_hodl_invoice_to_failed_by_r_hash( +pub fn update_hodl_invoice_to_canceled( conn: &mut PgConnection, r_hash: String, ) -> QueryResult { @@ -103,7 +104,7 @@ pub fn update_hodl_invoice_to_failed_by_r_hash( .filter(hodl_invoices::r_hash.eq(r_hash)) .set(( hodl_invoices::updated_at.eq(OffsetDateTime::now_utc()), - hodl_invoices::invoice_state.eq(InvoiceState::Failed), + hodl_invoices::invoice_state.eq(InvoiceState::Canceled), )) .execute(conn) } diff --git a/coordinator/src/node/invoice.rs b/coordinator/src/node/invoice.rs index 04bf8a99a..b20262228 100644 --- a/coordinator/src/node/invoice.rs +++ b/coordinator/src/node/invoice.rs @@ -41,7 +41,7 @@ pub fn spawn_invoice_watch( let r_hash = r_hash.clone(); move || { let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed_by_r_hash( + db::hodl_invoice::update_hodl_invoice_to_canceled( &mut conn, r_hash, )?; anyhow::Ok(()) From b113b561edde25a485e8c3d3133677c961347d2d Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 31 May 2024 16:05:52 +0200 Subject: [PATCH 4/4] feat: Set pending hodl invoice to canceled on startup. At the coordinator startup we stopped all tasks watching invoices. Thus we can safely set all `Open` or `Accepted` (pending) invoices to `Canceled`. --- coordinator/src/bin/coordinator.rs | 15 +++++++++++++++ coordinator/src/db/hodl_invoice.rs | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index eae15e487..cad9bda12 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -3,6 +3,7 @@ use anyhow::Result; use bitcoin::key::XOnlyPublicKey; use coordinator::backup::SledBackup; use coordinator::cli::Opts; +use coordinator::db; use coordinator::dlc_handler; use coordinator::dlc_handler::DlcHandler; use coordinator::logger; @@ -356,6 +357,20 @@ async fn main() -> Result<()> { } }); + if let Err(e) = spawn_blocking({ + let pool = pool.clone(); + move || { + let mut conn = pool.get()?; + db::hodl_invoice::cancel_pending_hodl_invoices(&mut conn)?; + anyhow::Ok(()) + } + }) + .await + .expect("task to finish") + { + tracing::error!("Failed to set expired hodl invoices to canceled. Error: {e:#}"); + } + tracing::debug!("Listening on http://{}", http_address); match axum::Server::bind(&http_address) diff --git a/coordinator/src/db/hodl_invoice.rs b/coordinator/src/db/hodl_invoice.rs index 033503cae..8f82edabe 100644 --- a/coordinator/src/db/hodl_invoice.rs +++ b/coordinator/src/db/hodl_invoice.rs @@ -35,6 +35,13 @@ impl QueryId for InvoiceStateType { } } +pub fn cancel_pending_hodl_invoices(conn: &mut PgConnection) -> QueryResult { + diesel::update(hodl_invoices::table) + .filter(hodl_invoices::invoice_state.eq_any([InvoiceState::Open, InvoiceState::Accepted])) + .set(hodl_invoices::invoice_state.eq(InvoiceState::Canceled)) + .execute(conn) +} + pub fn create_hodl_invoice( conn: &mut PgConnection, r_hash: &str,