From 34a81a9013af469f00a9970fa7dc9fe80a442a54 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 16:39:17 +0100 Subject: [PATCH 1/5] feat: persist paid BOLT12 invoices in payment details Co-Authored-By: Claude Opus 4.6 (1M context) --- src/event.rs | 5 +++- src/payment/bolt12.rs | 7 +++++ src/payment/store.rs | 51 +++++++++++++++++++++++++++------ tests/integration_tests_rust.rs | 8 +++++- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/event.rs b/src/event.rs index f06d701bc..0914ce714 100644 --- a/src/event.rs +++ b/src/event.rs @@ -860,6 +860,7 @@ where offer_id, payer_note, quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( @@ -1073,12 +1074,14 @@ where debug_assert!(false, "payment_id should always be set."); return Ok(()); }; + let bolt12_invoice = bolt12_invoice.map(Into::into); let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), fee_paid_msat: Some(fee_paid_msat), status: Some(PaymentStatus::Succeeded), + bolt12_invoice: Some(bolt12_invoice.clone()), ..PaymentDetailsUpdate::new(payment_id) }; @@ -1110,7 +1113,7 @@ where payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, - bolt12_invoice: bolt12_invoice.map(Into::into), + bolt12_invoice, }; match self.event_queue.add_event(event).await { diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 980e20696..5159f37cc 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -14,6 +14,7 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lightning::blinded_path::message::BlindedMessagePath; +use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; use lightning::ln::outbound_payment::Retry; use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; @@ -154,6 +155,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -179,6 +181,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -314,6 +317,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -339,6 +343,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, @@ -444,6 +449,7 @@ impl Bolt12Payment { secret: None, payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), quantity: refund.quantity(), + bolt12_invoice: Some(LdkPaidBolt12Invoice::Bolt12Invoice(invoice.clone()).into()), }; let payment = PaymentDetails::new( @@ -514,6 +520,7 @@ impl Bolt12Payment { secret: None, payer_note: payer_note.map(|note| UntrustedString(note)), quantity, + bolt12_invoice: None, }; let payment = PaymentDetails::new( payment_id, diff --git a/src/payment/store.rs b/src/payment/store.rs index 0e2de9815..a9585e9c3 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -8,6 +8,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bitcoin::{BlockHash, Txid}; +#[cfg(not(feature = "uniffi"))] +use lightning::events::PaidBolt12Invoice; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; @@ -20,6 +22,8 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +#[cfg(feature = "uniffi")] +use crate::ffi::PaidBolt12Invoice; use crate::hex_utils; /// Represents a payment. @@ -267,6 +271,18 @@ impl StorableObject for PaymentDetails { update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt); } + if let Some(ref bolt12_invoice_opt) = update.bolt12_invoice { + match self.kind { + PaymentKind::Bolt12Offer { ref mut bolt12_invoice, .. } => { + update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone()); + }, + PaymentKind::Bolt12Refund { ref mut bolt12_invoice, .. } => { + update_if_necessary!(*bolt12_invoice, bolt12_invoice_opt.clone()); + }, + _ => {}, + } + } + if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { match self.kind { PaymentKind::Bolt11Jit { ref mut counterparty_skimmed_fee_msat, .. } => { @@ -428,6 +444,8 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + /// The BOLT12 invoice associated with the payment, once available. + bolt12_invoice: Option, }, /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. /// @@ -448,6 +466,8 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + /// The BOLT12 invoice associated with the payment, once available. + bolt12_invoice: Option, }, /// A spontaneous ("keysend") payment. Spontaneous { @@ -482,6 +502,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (3, quantity, option), (4, secret, option), (6, offer_id, required), + (8, bolt12_invoice, option), }, (8, Spontaneous) => { (0, hash, required), @@ -493,6 +514,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (2, preimage, option), (3, quantity, option), (4, secret, option), + (6, bolt12_invoice, option), } ); @@ -555,6 +577,7 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, + pub bolt12_invoice: Option>, pub txid: Option, } @@ -571,6 +594,7 @@ impl PaymentDetailsUpdate { direction: None, status: None, confirmation_status: None, + bolt12_invoice: None, txid: None, } } @@ -578,13 +602,21 @@ impl PaymentDetailsUpdate { impl From<&PaymentDetails> for PaymentDetailsUpdate { fn from(value: &PaymentDetails) -> Self { - let (hash, preimage, secret) = match value.kind { - PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret), - PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => (Some(hash), preimage, secret), - PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret), - PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret), - PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None), - _ => (None, None, None), + let (hash, preimage, secret, bolt12_invoice) = match &value.kind { + PaymentKind::Bolt11 { hash, preimage, secret, .. } => { + (Some(*hash), *preimage, *secret, None) + }, + PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => { + (Some(*hash), *preimage, *secret, None) + }, + PaymentKind::Bolt12Offer { hash, preimage, secret, bolt12_invoice, .. } => { + (*hash, *preimage, *secret, Some(bolt12_invoice.clone())) + }, + PaymentKind::Bolt12Refund { hash, preimage, secret, bolt12_invoice, .. } => { + (*hash, *preimage, *secret, Some(bolt12_invoice.clone())) + }, + PaymentKind::Spontaneous { hash, preimage, .. } => (Some(*hash), *preimage, None, None), + _ => (None, None, None, None), }; let (confirmation_status, txid) = match &value.kind { @@ -592,9 +624,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => (None, None), }; - let counterparty_skimmed_fee_msat = match value.kind { + let counterparty_skimmed_fee_msat = match &value.kind { PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { - Some(counterparty_skimmed_fee_msat) + Some(*counterparty_skimmed_fee_msat) }, _ => None, }; @@ -610,6 +642,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { direction: Some(value.direction), status: Some(value.status), confirmation_status, + bolt12_invoice, txid, } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 413b2d44a..0e20020ff 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1115,12 +1115,14 @@ async fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. }, @@ -1182,12 +1184,14 @@ async fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(offer_id, offer.id()); assert_eq!(&expected_quantity, qty); assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. hash.unwrap() @@ -1255,11 +1259,13 @@ async fn simple_bolt12_send_receive() { secret: _, quantity: ref qty, payer_note: ref note, + bolt12_invoice: ref invoice, } => { assert!(hash.is_some()); assert!(preimage.is_some()); assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + assert!(invoice.is_some()); // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 // API currently doesn't allow to do that. }, From eb7c1a8355bc1b143fef698ed9d260b5b098ac3a Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 16:39:38 +0100 Subject: [PATCH 2/5] feat: add BOLT12 payer proof support Add payer-proof creation for outbound BOLT12 payments, persist the invoice context needed to build the proof, and expose the new types through the public and UniFFI surfaces. AI-assisted-by: Codex --- Cargo.toml | 48 ++++++------- bindings/ldk_node.udl | 3 + src/builder.rs | 48 +++++++++---- src/config.rs | 1 + src/error.rs | 11 +++ src/event.rs | 114 +++++++++++++++++++++++++---- src/ffi/types.rs | 87 ++++++++++++++++++++++ src/io/mod.rs | 4 ++ src/io/utils.rs | 76 ++++++++++++++++++++ src/lib.rs | 9 ++- src/payment/bolt12.rs | 119 +++++++++++++++++++++++++++++-- src/payment/mod.rs | 3 +- src/payment/payer_proof_store.rs | 108 ++++++++++++++++++++++++++++ src/types.rs | 2 + 14 files changed, 574 insertions(+), 59 deletions(-) create mode 100644 src/payment/payer_proof_store.rs diff --git a/Cargo.toml b/Cargo.toml index 8a85c6574..a26a1ee23 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -85,7 +85,7 @@ bitcoin-payment-instructions = { git = "https://github.com/joostjager/bitcoin-pa winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" @@ -172,15 +172,15 @@ harness = false #vss-client-ng = { path = "../vss-client" } #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } # -#[patch."https://github.com/lightningdevkit/rust-lightning"] -#lightning = { path = "../rust-lightning/lightning" } -#lightning-types = { path = "../rust-lightning/lightning-types" } -#lightning-invoice = { path = "../rust-lightning/lightning-invoice" } -#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister" } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } -#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } -#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } -#lightning-macros = { path = "../rust-lightning/lightning-macros" } +[patch."https://github.com/lightningdevkit/rust-lightning"] +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 014993690..29ed4df94 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -185,6 +185,8 @@ enum NodeError { "FeerateEstimationUpdateTimeout", "WalletOperationFailed", "WalletOperationTimeout", + "PayerProofCreationFailed", + "PayerProofUnavailable", "OnchainTxSigningFailed", "TxSyncFailed", "TxSyncTimeout", @@ -225,6 +227,7 @@ enum NodeError { "LnurlAuthFailed", "LnurlAuthTimeout", "InvalidLnurl", + "InvalidPayerProof", }; typedef dictionary NodeStatus; diff --git a/src/builder.rs b/src/builder.rs index cd8cc184f..df54d49fd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -56,12 +56,14 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph, - read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments, - read_scorer, write_node_metrics, + read_node_metrics, read_output_sweeper, read_payer_proof_contexts, read_payments, + read_peer_info, read_pending_payments, read_scorer, write_node_metrics, }; use crate::io::vss_store::VssStoreBuilder; use crate::io::{ - self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + self, PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; @@ -77,8 +79,8 @@ use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, - GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, - PendingPaymentStore, SyncAndAsyncKVStore, + GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PayerProofContextStore, + PaymentStore, PeerManager, PendingPaymentStore, SyncAndAsyncKVStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -1260,14 +1262,19 @@ fn build_with_store_internal( let kv_store_ref = Arc::clone(&kv_store); let logger_ref = Arc::clone(&logger); - let (payment_store_res, node_metris_res, pending_payment_store_res) = - runtime.block_on(async move { - tokio::join!( - read_payments(&*kv_store_ref, Arc::clone(&logger_ref)), - read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), - read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)) - ) - }); + let ( + payment_store_res, + node_metris_res, + pending_payment_store_res, + payer_proof_context_store_res, + ) = runtime.block_on(async move { + tokio::join!( + read_payments(&*kv_store_ref, Arc::clone(&logger_ref)), + read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), + read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)), + read_payer_proof_contexts(&*kv_store_ref, Arc::clone(&logger_ref)) + ) + }); // Initialize the status fields. let node_metrics = match node_metris_res { @@ -1296,6 +1303,20 @@ fn build_with_store_internal( }, }; + let payer_proof_context_store = match payer_proof_context_store_res { + Ok(contexts) => Arc::new(PayerProofContextStore::new( + contexts, + PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), + Err(e) => { + log_error!(logger, "Failed to read payer proof contexts from store: {}", e); + return Err(BuildError::ReadFailed); + }, + }; + let (chain_source, chain_tip_opt) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default()); @@ -1987,6 +2008,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + payer_proof_context_store, lnurl_auth, is_running, node_metrics, diff --git a/src/config.rs b/src/config.rs index 71e4d2314..8300c4882 100644 --- a/src/config.rs +++ b/src/config.rs @@ -342,6 +342,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); user_config.reject_inbound_splices = false; + user_config.manually_handle_bolt12_invoices = true; if may_announce_channel(config).is_err() { user_config.accept_forwards_to_priv_channels = false; diff --git a/src/error.rs b/src/error.rs index d07212b00..1fdef8360 100644 --- a/src/error.rs +++ b/src/error.rs @@ -57,6 +57,10 @@ pub enum Error { WalletOperationFailed, /// A wallet operation timed out. WalletOperationTimeout, + /// Creating a payer proof failed. + PayerProofCreationFailed, + /// A payer proof is unavailable for the requested payment. + PayerProofUnavailable, /// A signing operation for transaction failed. OnchainTxSigningFailed, /// A transaction sync operation failed. @@ -137,6 +141,8 @@ pub enum Error { LnurlAuthTimeout, /// The provided lnurl is invalid. InvalidLnurl, + /// The provided payer proof is invalid. + InvalidPayerProof, } impl fmt::Display for Error { @@ -168,6 +174,10 @@ impl fmt::Display for Error { }, Self::WalletOperationFailed => write!(f, "Failed to conduct wallet operation."), Self::WalletOperationTimeout => write!(f, "A wallet operation timed out."), + Self::PayerProofCreationFailed => write!(f, "Failed to create payer proof."), + Self::PayerProofUnavailable => { + write!(f, "A payer proof is unavailable for the requested payment.") + }, Self::OnchainTxSigningFailed => write!(f, "Failed to sign given transaction."), Self::TxSyncFailed => write!(f, "Failed to sync transactions."), Self::TxSyncTimeout => write!(f, "Syncing transactions timed out."), @@ -222,6 +232,7 @@ impl fmt::Display for Error { Self::LnurlAuthFailed => write!(f, "LNURL-auth authentication failed."), Self::LnurlAuthTimeout => write!(f, "LNURL-auth authentication timed out."), Self::InvalidLnurl => write!(f, "The provided lnurl is invalid."), + Self::InvalidPayerProof => write!(f, "The provided payer proof is invalid."), } } } diff --git a/src/event.rs b/src/event.rs index 0914ce714..1473b3254 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex}; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; +use lightning::blinded_path::message::OffersContext; use lightning::events::bump_transaction::BumpTransactionEvent; #[cfg(not(feature = "uniffi"))] use lightning::events::PaidBolt12Invoice; @@ -23,7 +24,13 @@ use lightning::events::{ }; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; +use lightning::ln::outbound_payment::Bolt12PaymentError; use lightning::ln::types::ChannelId; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::invoice_error::InvoiceError; +use lightning::offers::parse::Bolt12SemanticError; +use lightning::onion_message::messenger::Responder; +use lightning::onion_message::offers::OffersMessage; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; use lightning::util::config::{ @@ -49,12 +56,14 @@ use crate::liquidity::LiquiditySource; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; +use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; use crate::runtime::Runtime; use crate::types::{ - CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, + CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PayerProofContextStore, PaymentStore, + Sweeper, Wallet, }; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, @@ -507,6 +516,7 @@ where network_graph: Arc, liquidity_source: Option>>>, payment_store: Arc, + payer_proof_context_store: Arc, peer_store: Arc>, keys_manager: Arc, runtime: Arc, @@ -527,10 +537,11 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc, peer_store: Arc>, - keys_manager: Arc, static_invoice_store: Option, - onion_messenger: Arc, om_mailbox: Option>, - runtime: Arc, logger: L, config: Arc, + payment_store: Arc, payer_proof_context_store: Arc, + peer_store: Arc>, keys_manager: Arc, + static_invoice_store: Option, onion_messenger: Arc, + om_mailbox: Option>, runtime: Arc, logger: L, + config: Arc, ) -> Self { Self { event_queue, @@ -542,6 +553,7 @@ where network_graph, liquidity_source, payment_store, + payer_proof_context_store, peer_store, keys_manager, logger, @@ -553,6 +565,66 @@ where } } + fn persist_payer_proof_context( + &self, payment_id: PaymentId, invoice: &Bolt12Invoice, context: Option<&OffersContext>, + ) -> Result<(), ReplayEvent> { + if let Some(context) = + PayerProofContext::from_invoice_received(payment_id, invoice, context) + { + match self.payer_proof_context_store.insert_or_update(context) { + Ok(_) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to persist payer proof context for {}: {}", + payment_id, + e + ); + return Err(ReplayEvent()); + }, + } + } + + Ok(()) + } + + fn remove_payer_proof_context(&self, payment_id: &PaymentId) -> Result<(), ReplayEvent> { + self.payer_proof_context_store.remove(payment_id).map_err(|e| { + log_error!( + self.logger, + "Failed to remove payer proof context for {}: {}", + payment_id, + e + ); + ReplayEvent() + }) + } + + fn respond_to_invoice_error(&self, error: Bolt12PaymentError, responder: Option) { + let error = match error { + Bolt12PaymentError::UnknownRequiredFeatures => { + InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) + }, + Bolt12PaymentError::SendingFailed(e) => InvoiceError::from_string(format!("{:?}", e)), + Bolt12PaymentError::BlindedPathCreationFailed => InvoiceError::from_string( + "Failed to create a blinded path back to ourselves".to_string(), + ), + Bolt12PaymentError::UnexpectedInvoice | Bolt12PaymentError::DuplicateInvoice => return, + }; + + let Some(responder) = responder else { + log_trace!(self.logger, "No reply path available for invoice_error response"); + return; + }; + + if let Err(e) = self + .onion_messenger + .handle_onion_message_response(OffersMessage::InvoiceError(error), responder.respond()) + { + log_error!(self.logger, "Failed to send invoice_error response: {:?}", e); + } + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -1144,6 +1216,7 @@ where return Err(ReplayEvent()); }, }; + self.remove_payer_proof_context(&payment_id)?; let event = Event::PaymentFailed { payment_id: Some(payment_id), payment_hash, reason }; @@ -1565,20 +1638,14 @@ where }; }, LdkEvent::DiscardFunding { channel_id, funding_info } => { - if let FundingInfo::Contribution { inputs: _, outputs } = funding_info { + if let FundingInfo::Tx { transaction } = funding_info { log_info!( self.logger, "Reclaiming unused addresses from channel {} funding", channel_id, ); - let tx = bitcoin::Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: outputs, - }; - if let Err(e) = self.wallet.cancel_tx(&tx) { + if let Err(e) = self.wallet.cancel_tx(&transaction) { log_error!(self.logger, "Failed reclaiming unused addresses: {}", e); return Err(ReplayEvent()); } @@ -1602,8 +1669,25 @@ where .await; } }, - LdkEvent::InvoiceReceived { .. } => { - debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); + LdkEvent::InvoiceReceived { payment_id, invoice, context, responder } => { + self.persist_payer_proof_context(payment_id, &invoice, context.as_ref())?; + + match self + .channel_manager + .send_payment_for_bolt12_invoice(&invoice, context.as_ref()) + { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to initiate payment for BOLT12 invoice {}: {:?}", + payment_id, + e + ); + self.remove_payer_proof_context(&payment_id)?; + self.respond_to_invoice_error(e, responder); + }, + } }, LdkEvent::ConnectionNeeded { node_id, addresses } => { let spawn_logger = self.logger.clone(); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 5a1420882..70796bd5a 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -31,6 +31,7 @@ pub use lightning::ln::types::ChannelId; use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; pub use lightning::offers::offer::OfferId; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; +use lightning::offers::payer_proof::PayerProof as LdkPayerProof; use lightning::offers::refund::Refund as LdkRefund; use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice; use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; @@ -875,6 +876,92 @@ impl Readable for PaidBolt12Invoice { } } +/// A cryptographic proof that a BOLT12 invoice was paid by this node. +#[derive(Debug, Clone, uniffi::Object)] +#[uniffi::export(Debug, Display)] +pub struct PayerProof { + pub(crate) inner: LdkPayerProof, +} + +#[uniffi::export] +impl PayerProof { + #[uniffi::constructor] + pub fn from_bytes(proof_bytes: Vec) -> Result { + let inner = LdkPayerProof::try_from(proof_bytes).map_err(|_| Error::InvalidPayerProof)?; + Ok(Self { inner }) + } + + /// The payment preimage proving the payment completed. + pub fn preimage(&self) -> PaymentPreimage { + self.inner.preimage() + } + + /// The payment hash committed to by the invoice and proven by the preimage. + pub fn payment_hash(&self) -> PaymentHash { + self.inner.payment_hash() + } + + /// The public key of the payer that authorized the payment. + pub fn payer_id(&self) -> PublicKey { + self.inner.payer_id() + } + + /// The issuer signing public key committed to by the invoice. + pub fn issuer_signing_pubkey(&self) -> PublicKey { + self.inner.issuer_signing_pubkey() + } + + /// The invoice signature bytes. + pub fn invoice_signature(&self) -> Vec { + self.inner.invoice_signature().as_ref().to_vec() + } + + /// The payer signature bytes. + pub fn payer_signature(&self) -> Vec { + self.inner.payer_signature().as_ref().to_vec() + } + + /// The optional note attached to the proof. + pub fn payer_note(&self) -> Option { + self.inner.payer_note().map(|value| value.to_string()) + } + + /// The Merkle root committed to by the proof. + pub fn merkle_root(&self) -> Vec { + self.inner.merkle_root().to_byte_array().to_vec() + } + + /// The raw TLV bytes of the proof. + pub fn bytes(&self) -> Vec { + self.inner.bytes().to_vec() + } + + /// The bech32-encoded string form of the proof. + pub fn as_string(&self) -> String { + self.inner.to_string() + } +} + +impl From for PayerProof { + fn from(inner: LdkPayerProof) -> Self { + Self { inner } + } +} + +impl Deref for PayerProof { + type Target = LdkPayerProof; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::fmt::Display for PayerProof { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + uniffi::custom_type!(OfferId, String, { remote, try_lift: |val| { diff --git a/src/io/mod.rs b/src/io/mod.rs index e080d39f7..d5966d61f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -82,3 +82,7 @@ pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices /// The pending payment information will be persisted under this prefix. pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments"; pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; + +/// The payer proof context will be persisted under this prefix. +pub(crate) const PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payer_proof_contexts"; +pub(crate) const PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/io/utils.rs b/src/io/utils.rs index eef71ec0b..2915f1d57 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -42,8 +42,11 @@ use crate::config::WALLET_KEYS_SEED_LEN; use crate::fee_estimator::OnchainFeeEstimator; use crate::io::{ NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::PendingPaymentDetails; use crate::peer_store::PeerStore; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; @@ -298,6 +301,79 @@ where Ok(res) } +/// Read previously persisted payer proof contexts from the store. +pub(crate) async fn read_payer_proof_contexts( + kv_store: &DynStore, logger: L, +) -> Result, std::io::Error> +where + L::Target: LdkLogger, +{ + let mut res = Vec::new(); + + let mut stored_keys = KVStore::list( + &*kv_store, + PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, + ) + .await?; + + const BATCH_SIZE: usize = 50; + + let mut set = tokio::task::JoinSet::new(); + + while set.len() < BATCH_SIZE && !stored_keys.is_empty() { + if let Some(next_key) = stored_keys.pop() { + let fut = KVStore::read( + &*kv_store, + PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, + &next_key, + ); + set.spawn(fut); + debug_assert!(set.len() <= BATCH_SIZE); + } + } + + while let Some(read_res) = set.join_next().await { + let reader = read_res + .map_err(|e| { + log_error!(logger, "Failed to read PayerProofContext: {}", e); + set.abort_all(); + e + })? + .map_err(|e| { + log_error!(logger, "Failed to read PayerProofContext: {}", e); + set.abort_all(); + e + })?; + + if let Some(next_key) = stored_keys.pop() { + let fut = KVStore::read( + &*kv_store, + PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, + PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, + &next_key, + ); + set.spawn(fut); + debug_assert!(set.len() <= BATCH_SIZE); + } + + let context = PayerProofContext::read(&mut &*reader).map_err(|e| { + log_error!(logger, "Failed to deserialize PayerProofContext: {}", e); + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to deserialize PayerProofContext", + ) + })?; + res.push(context); + } + + debug_assert!(set.is_empty()); + debug_assert!(stored_keys.is_empty()); + + Ok(res) +} + /// Read `OutputSweeper` state from the store. pub(crate) async fn read_output_sweeper( broadcaster: Arc, fee_estimator: Arc, diff --git a/src/lib.rs b/src/lib.rs index 2ac4697e8..c949100af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,8 +173,8 @@ use runtime::Runtime; pub use tokio; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, - Wallet, + HRNResolver, KeysManager, OnionMessenger, PayerProofContextStore, PaymentStore, PeerManager, + Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId}; pub use vss_client; @@ -232,6 +232,7 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc, + payer_proof_context_store: Arc, lnurl_auth: Arc, is_running: Arc>, node_metrics: Arc>, @@ -585,6 +586,7 @@ impl Node { Arc::clone(&self.network_graph), self.liquidity_source.clone(), Arc::clone(&self.payment_store), + Arc::clone(&self.payer_proof_context_store), Arc::clone(&self.peer_store), Arc::clone(&self.keys_manager), static_invoice_store, @@ -904,6 +906,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.payer_proof_context_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -920,6 +923,7 @@ impl Node { Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.payer_proof_context_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -1776,6 +1780,7 @@ impl Node { /// Remove the payment with the given id from the store. pub fn remove_payment(&self, payment_id: &PaymentId) -> Result<(), Error> { + self.payer_proof_context_store.remove(&payment_id)?; self.payment_store.remove(&payment_id) } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 5159f37cc..3b408c3b9 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -19,8 +19,10 @@ use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; use lightning::ln::outbound_payment::Retry; use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; use lightning::offers::parse::Bolt12SemanticError; +#[cfg(not(feature = "uniffi"))] +use lightning::offers::payer_proof::PayerProof as LdkPayerProof; use lightning::routing::router::RouteParametersConfig; -use lightning::sign::EntropySource; +use lightning::sign::{EntropySource, NodeSigner}; #[cfg(feature = "uniffi")] use lightning::util::ser::{Readable, Writeable}; use lightning_types::string::UntrustedString; @@ -29,8 +31,9 @@ use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; +use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, PayerProofContextStore, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; @@ -52,6 +55,11 @@ type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadable #[cfg(feature = "uniffi")] type HumanReadableName = Arc; +#[cfg(not(feature = "uniffi"))] +type PayerProof = LdkPayerProof; +#[cfg(feature = "uniffi")] +type PayerProof = Arc; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -63,22 +71,43 @@ pub struct Bolt12Payment { channel_manager: Arc, keys_manager: Arc, payment_store: Arc, + payer_proof_context_store: Arc, config: Arc, is_running: Arc>, logger: Arc, async_payments_role: Option, } +/// Options controlling which optional fields are disclosed in a BOLT12 payer proof. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct PayerProofOptions { + /// An optional note attached to the payer proof itself. + pub note: Option, + /// Whether to include the offer description in the proof. + pub include_offer_description: bool, + /// Whether to include the offer issuer in the proof. + pub include_offer_issuer: bool, + /// Whether to include the invoice amount in the proof. + pub include_invoice_amount: bool, + /// Whether to include the invoice creation timestamp in the proof. + pub include_invoice_created_at: bool, + /// Additional TLV types to include in the selective disclosure set. + pub extra_tlv_types: Vec, +} + impl Bolt12Payment { pub(crate) fn new( channel_manager: Arc, keys_manager: Arc, - payment_store: Arc, config: Arc, is_running: Arc>, - logger: Arc, async_payments_role: Option, + payment_store: Arc, payer_proof_context_store: Arc, + config: Arc, is_running: Arc>, logger: Arc, + async_payments_role: Option, ) -> Self { Self { channel_manager, keys_manager, payment_store, + payer_proof_context_store, config, is_running, logger, @@ -248,6 +277,21 @@ impl Bolt12Payment { .blinded_paths_for_async_recipient(recipient_id, None) .or(Err(Error::InvalidBlindedPaths)) } + + fn payer_proof_context( + &self, payment_id: &PaymentId, + ) -> Result<(PaymentDetails, PayerProofContext), Error> { + let payment = self.payment_store.get(payment_id).ok_or(Error::PayerProofUnavailable)?; + if payment.direction != PaymentDirection::Outbound + || payment.status != PaymentStatus::Succeeded + { + return Err(Error::PayerProofUnavailable); + } + + let context = + self.payer_proof_context_store.get(payment_id).ok_or(Error::PayerProofUnavailable)?; + Ok((payment, context)) + } } #[cfg_attr(feature = "uniffi", uniffi::export)] @@ -388,6 +432,73 @@ impl Bolt12Payment { Ok(payment_id) } + /// Create a payer proof for a previously succeeded outbound BOLT12 payment. + /// + /// This requires a standard BOLT12 invoice response that carried payer proof context. + /// Payments that completed via static invoices do not support payer proofs. + pub fn create_payer_proof( + &self, payment_id: &PaymentId, options: Option, + ) -> Result { + let (payment, context) = self.payer_proof_context(payment_id)?; + let preimage = match payment.kind { + PaymentKind::Bolt12Offer { preimage: Some(preimage), .. } + | PaymentKind::Bolt12Refund { preimage: Some(preimage), .. } => preimage, + _ => return Err(Error::PayerProofUnavailable), + }; + + let options = options.unwrap_or_default(); + let mut builder = context.invoice.payer_proof_builder(preimage).map_err(|e| { + log_error!( + self.logger, + "Failed to initialize payer proof builder for {}: {:?}", + payment_id, + e + ); + Error::PayerProofCreationFailed + })?; + + for tlv_type in options.extra_tlv_types { + builder = builder.include_type(tlv_type).map_err(|e| { + log_error!( + self.logger, + "Failed to include TLV {} in payer proof for {}: {:?}", + tlv_type, + payment_id, + e + ); + Error::PayerProofCreationFailed + })?; + } + + if options.include_offer_description { + builder = builder.include_offer_description(); + } + if options.include_offer_issuer { + builder = builder.include_offer_issuer(); + } + if options.include_invoice_amount { + builder = builder.include_invoice_amount(); + } + if options.include_invoice_created_at { + builder = builder.include_invoice_created_at(); + } + + let expanded_key = self.keys_manager.get_expanded_key(); + let proof = builder + .build_with_derived_key( + &expanded_key, + context.nonce, + *payment_id, + options.note.as_deref(), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to build payer proof for {}: {:?}", payment_id, e); + Error::PayerProofCreationFailed + })?; + + Ok(maybe_wrap(proof)) + } + /// Returns a payable offer that can be used to request and receive a payment of the amount /// given. pub fn receive( diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 42b5aff3b..86a3f1154 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -11,13 +11,14 @@ pub(crate) mod asynchronous; mod bolt11; mod bolt12; mod onchain; +pub(crate) mod payer_proof_store; pub(crate) mod pending_payment_store; mod spontaneous; pub(crate) mod store; mod unified; pub use bolt11::Bolt11Payment; -pub use bolt12::Bolt12Payment; +pub use bolt12::{Bolt12Payment, PayerProofOptions}; pub use onchain::OnchainPayment; pub use pending_payment_store::PendingPaymentDetails; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/payer_proof_store.rs b/src/payment/payer_proof_store.rs new file mode 100644 index 000000000..d50131386 --- /dev/null +++ b/src/payment/payer_proof_store.rs @@ -0,0 +1,108 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use lightning::blinded_path::message::OffersContext; +use lightning::impl_writeable_tlv_based; +use lightning::ln::channelmanager::PaymentId; +use lightning::offers::invoice::Bolt12Invoice; +use lightning::offers::nonce::Nonce; + +use crate::data_store::{StorableObject, StorableObjectUpdate}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PayerProofContext { + pub payment_id: PaymentId, + pub invoice: Bolt12Invoice, + pub nonce: Nonce, +} + +impl PayerProofContext { + pub(crate) fn from_invoice_received( + payment_id: PaymentId, invoice: &Bolt12Invoice, context: Option<&OffersContext>, + ) -> Option { + match context { + Some(OffersContext::OutboundPaymentForOffer { + payment_id: context_payment_id, + nonce, + }) + | Some(OffersContext::OutboundPaymentForRefund { + payment_id: context_payment_id, + nonce, + }) if *context_payment_id == payment_id => { + Some(Self { payment_id, invoice: invoice.clone(), nonce: *nonce }) + }, + _ => None, + } + } +} + +impl_writeable_tlv_based!(PayerProofContext, { + (0, payment_id, required), + (2, invoice, required), + (4, nonce, required), +}); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PayerProofContextUpdate { + pub payment_id: PaymentId, + pub invoice: Option, + pub nonce: Option, +} + +impl From<&PayerProofContext> for PayerProofContextUpdate { + fn from(value: &PayerProofContext) -> Self { + Self { + payment_id: value.payment_id, + invoice: Some(value.invoice.clone()), + nonce: Some(value.nonce), + } + } +} + +impl StorableObject for PayerProofContext { + type Id = PaymentId; + type Update = PayerProofContextUpdate; + + fn id(&self) -> Self::Id { + self.payment_id + } + + fn update(&mut self, update: Self::Update) -> bool { + debug_assert_eq!( + self.payment_id, update.payment_id, + "We should only ever override payer proof context for the same payment id" + ); + + let mut updated = false; + + if let Some(invoice) = update.invoice { + if self.invoice != invoice { + self.invoice = invoice; + updated = true; + } + } + + if let Some(nonce) = update.nonce { + if self.nonce != nonce { + self.nonce = nonce; + updated = true; + } + } + + updated + } + + fn to_update(&self) -> Self::Update { + self.into() + } +} + +impl StorableObjectUpdate for PayerProofContextUpdate { + fn id(&self) -> ::Id { + self.payment_id + } +} diff --git a/src/types.rs b/src/types.rs index dae315ae0..04650cfc6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -37,6 +37,7 @@ use crate::data_store::DataStore; use crate::fee_estimator::OnchainFeeEstimator; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::{PaymentDetails, PendingPaymentDetails}; use crate::runtime::RuntimeSpawner; @@ -349,6 +350,7 @@ pub(crate) type BumpTransactionEventHandler = >; pub(crate) type PaymentStore = DataStore>; +pub(crate) type PayerProofContextStore = DataStore>; /// A local, potentially user-provided, identifier of a channel. /// From 6455eee0d8d3ae9c7ce5d257ccc526173bb08b4b Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 26 Mar 2026 18:45:38 +0100 Subject: [PATCH 3/5] refactor: use payment_nonce from PaymentSent for payer proofs Update rust-lightning dependency to include commits from PRs #4210 (BLIP-42 contact secrets) and #4297 (payment_nonce in PaymentSent). The payment_nonce in PaymentSent allows capturing the nonce needed for payer proof construction directly from the payment success event, eliminating the need to set manually_handle_bolt12_invoices and intercept InvoiceReceived. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 46 +++++++------- src/config.rs | 1 - src/event.rs | 100 ++++++++----------------------- src/payment/bolt12.rs | 4 ++ src/payment/payer_proof_store.rs | 21 ------- 5 files changed, 51 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a26a1ee23..ebaf1ef2a 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } -lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std"] } -lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -85,7 +85,7 @@ bitcoin-payment-instructions = { git = "https://github.com/joostjager/bitcoin-pa winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" @@ -173,14 +173,14 @@ harness = false #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } # [patch."https://github.com/lightningdevkit/rust-lightning"] -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } -lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "d5baee83aee0e2674020700f77918f46b3925ab6" } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } diff --git a/src/config.rs b/src/config.rs index 8300c4882..71e4d2314 100644 --- a/src/config.rs +++ b/src/config.rs @@ -342,7 +342,6 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); user_config.reject_inbound_splices = false; - user_config.manually_handle_bolt12_invoices = true; if may_announce_channel(config).is_err() { user_config.accept_forwards_to_priv_channels = false; diff --git a/src/event.rs b/src/event.rs index 1473b3254..e56c93e09 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,7 +14,6 @@ use std::sync::{Arc, Mutex}; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; -use lightning::blinded_path::message::OffersContext; use lightning::events::bump_transaction::BumpTransactionEvent; #[cfg(not(feature = "uniffi"))] use lightning::events::PaidBolt12Invoice; @@ -24,13 +23,8 @@ use lightning::events::{ }; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; -use lightning::ln::outbound_payment::Bolt12PaymentError; use lightning::ln::types::ChannelId; -use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::invoice_error::InvoiceError; -use lightning::offers::parse::Bolt12SemanticError; -use lightning::onion_message::messenger::Responder; -use lightning::onion_message::offers::OffersMessage; +use lightning::offers::nonce::Nonce; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; use lightning::util::config::{ @@ -566,62 +560,27 @@ where } fn persist_payer_proof_context( - &self, payment_id: PaymentId, invoice: &Bolt12Invoice, context: Option<&OffersContext>, - ) -> Result<(), ReplayEvent> { - if let Some(context) = - PayerProofContext::from_invoice_received(payment_id, invoice, context) - { - match self.payer_proof_context_store.insert_or_update(context) { - Ok(_) => {}, - Err(e) => { - log_error!( - self.logger, - "Failed to persist payer proof context for {}: {}", - payment_id, - e - ); - return Err(ReplayEvent()); - }, - } - } + &self, payment_id: PaymentId, bolt12_invoice: &Option, + payment_nonce: Option, + ) { + let invoice = match bolt12_invoice { + Some(PaidBolt12Invoice::Bolt12Invoice(invoice)) => invoice, + _ => return, + }; - Ok(()) - } + let nonce = match payment_nonce { + Some(nonce) => nonce, + None => return, + }; - fn remove_payer_proof_context(&self, payment_id: &PaymentId) -> Result<(), ReplayEvent> { - self.payer_proof_context_store.remove(payment_id).map_err(|e| { + let context = PayerProofContext { payment_id, invoice: invoice.clone(), nonce }; + if let Err(e) = self.payer_proof_context_store.insert_or_update(context) { log_error!( self.logger, - "Failed to remove payer proof context for {}: {}", + "Failed to persist payer proof context for {}: {}", payment_id, e ); - ReplayEvent() - }) - } - - fn respond_to_invoice_error(&self, error: Bolt12PaymentError, responder: Option) { - let error = match error { - Bolt12PaymentError::UnknownRequiredFeatures => { - InvoiceError::from(Bolt12SemanticError::UnknownRequiredFeatures) - }, - Bolt12PaymentError::SendingFailed(e) => InvoiceError::from_string(format!("{:?}", e)), - Bolt12PaymentError::BlindedPathCreationFailed => InvoiceError::from_string( - "Failed to create a blinded path back to ourselves".to_string(), - ), - Bolt12PaymentError::UnexpectedInvoice | Bolt12PaymentError::DuplicateInvoice => return, - }; - - let Some(responder) = responder else { - log_trace!(self.logger, "No reply path available for invoice_error response"); - return; - }; - - if let Err(e) = self - .onion_messenger - .handle_onion_message_response(OffersMessage::InvoiceError(error), responder.respond()) - { - log_error!(self.logger, "Failed to send invoice_error response: {:?}", e); } } @@ -1138,6 +1097,7 @@ where payment_hash, fee_paid_msat, bolt12_invoice, + payment_nonce, .. } => { let payment_id = if let Some(id) = payment_id { @@ -1148,6 +1108,12 @@ where }; let bolt12_invoice = bolt12_invoice.map(Into::into); + self.persist_payer_proof_context( + payment_id, + &bolt12_invoice, + payment_nonce, + ); + let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), @@ -1216,7 +1182,6 @@ where return Err(ReplayEvent()); }, }; - self.remove_payer_proof_context(&payment_id)?; let event = Event::PaymentFailed { payment_id: Some(payment_id), payment_hash, reason }; @@ -1669,25 +1634,8 @@ where .await; } }, - LdkEvent::InvoiceReceived { payment_id, invoice, context, responder } => { - self.persist_payer_proof_context(payment_id, &invoice, context.as_ref())?; - - match self - .channel_manager - .send_payment_for_bolt12_invoice(&invoice, context.as_ref()) - { - Ok(()) => {}, - Err(e) => { - log_error!( - self.logger, - "Failed to initiate payment for BOLT12 invoice {}: {:?}", - payment_id, - e - ); - self.remove_payer_proof_context(&payment_id)?; - self.respond_to_invoice_error(e, responder); - }, - } + LdkEvent::InvoiceReceived { .. } => { + debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, LdkEvent::ConnectionNeeded { node_id, addresses } => { let spawn_logger = self.logger.clone(); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 3b408c3b9..074d74aed 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -150,6 +150,8 @@ impl Bolt12Payment { payer_note: payer_note.clone(), retry_strategy, route_params_config: route_parameters, + contact_secrets: None, + payer_offer: None, }; let res = if let Some(hrn) = hrn { let hrn = maybe_deref(&hrn); @@ -336,6 +338,8 @@ impl Bolt12Payment { payer_note: payer_note.clone(), retry_strategy, route_params_config: route_parameters, + contact_secrets: None, + payer_offer: None, }; let res = if let Some(quantity) = quantity { self.channel_manager diff --git a/src/payment/payer_proof_store.rs b/src/payment/payer_proof_store.rs index d50131386..2ab36633b 100644 --- a/src/payment/payer_proof_store.rs +++ b/src/payment/payer_proof_store.rs @@ -5,7 +5,6 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::blinded_path::message::OffersContext; use lightning::impl_writeable_tlv_based; use lightning::ln::channelmanager::PaymentId; use lightning::offers::invoice::Bolt12Invoice; @@ -20,26 +19,6 @@ pub(crate) struct PayerProofContext { pub nonce: Nonce, } -impl PayerProofContext { - pub(crate) fn from_invoice_received( - payment_id: PaymentId, invoice: &Bolt12Invoice, context: Option<&OffersContext>, - ) -> Option { - match context { - Some(OffersContext::OutboundPaymentForOffer { - payment_id: context_payment_id, - nonce, - }) - | Some(OffersContext::OutboundPaymentForRefund { - payment_id: context_payment_id, - nonce, - }) if *context_payment_id == payment_id => { - Some(Self { payment_id, invoice: invoice.clone(), nonce: *nonce }) - }, - _ => None, - } - } -} - impl_writeable_tlv_based!(PayerProofContext, { (0, payment_id, required), (2, invoice, required), From dc6012f37cb56f02e240ac782f16d9e16484e06a Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 2 Apr 2026 10:29:02 +0200 Subject: [PATCH 4/5] fix: resolve CI failures for BOLT12 payer proof FFI build - Use cfg-gated match to extract Bolt12Invoice correctly per build mode: non-uniffi uses PaidBolt12Invoice::Bolt12Invoice variant, uniffi uses PaidBolt12Invoice::Bolt12 variant and unwraps Arc + inner - Apply cargo fmt to fix formatting diffs - Remove redundant .clone() in PayerProofContext construction Co-Authored-By: Claude --- src/event.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/event.rs b/src/event.rs index e56c93e09..cbad7ae08 100644 --- a/src/event.rs +++ b/src/event.rs @@ -563,8 +563,14 @@ where &self, payment_id: PaymentId, bolt12_invoice: &Option, payment_nonce: Option, ) { + #[cfg(not(feature = "uniffi"))] let invoice = match bolt12_invoice { - Some(PaidBolt12Invoice::Bolt12Invoice(invoice)) => invoice, + Some(PaidBolt12Invoice::Bolt12Invoice(invoice)) => invoice.clone(), + _ => return, + }; + #[cfg(feature = "uniffi")] + let invoice = match bolt12_invoice { + Some(PaidBolt12Invoice::Bolt12(invoice)) => invoice.inner.clone(), _ => return, }; @@ -573,7 +579,7 @@ where None => return, }; - let context = PayerProofContext { payment_id, invoice: invoice.clone(), nonce }; + let context = PayerProofContext { payment_id, invoice, nonce }; if let Err(e) = self.payer_proof_context_store.insert_or_update(context) { log_error!( self.logger, @@ -1108,11 +1114,7 @@ where }; let bolt12_invoice = bolt12_invoice.map(Into::into); - self.persist_payer_proof_context( - payment_id, - &bolt12_invoice, - payment_nonce, - ); + self.persist_payer_proof_context(payment_id, &bolt12_invoice, payment_nonce); let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), From a0641a2ec762c3c40e95fec1ec4a14ccfde445b8 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 2 Apr 2026 12:41:28 +0200 Subject: [PATCH 5/5] Update BOLT12 payer proof API for upstream LDK PR #4297 - Bump rust-lightning rev from f5c24c6 to c836395 - PaidBolt12Invoice changed from enum (lightning::events) to struct (lightning::offers::payer_proof) - New Bolt12InvoiceType enum replaces old PaidBolt12Invoice variants - OptionalOfferPaymentParams lost contact_secrets/payer_offer fields - PayerProofContextStore is now in-memory only since PaidBolt12Invoice cannot be serialized or reconstructed externally (new/nonce/invoice_type are all pub(crate) in LDK) - Removed payment_nonce field from Event::PaymentSuccessful - Removed read_payer_proof_contexts and related persistence constants - cfg-gated Bolt12InvoiceType vs FFI PaidBolt12Invoice for uniffi compat Co-authored-by: AI Assistant --- Cargo.toml | 46 +++++++-------- src/builder.rs | 43 ++++---------- src/event.rs | 76 ++++++++++++------------- src/ffi/types.rs | 51 +++++++++-------- src/io/mod.rs | 4 -- src/io/utils.rs | 76 ------------------------- src/lib.rs | 2 +- src/payment/bolt12.rs | 59 ++++++++++--------- src/payment/payer_proof_store.rs | 98 +++++++++----------------------- src/payment/store.rs | 15 +++-- src/types.rs | 3 +- 11 files changed, 168 insertions(+), 305 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ebaf1ef2a..89fc93a1e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } -lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std"] } -lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["std"] } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["std"] } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -85,7 +85,7 @@ bitcoin-payment-instructions = { git = "https://github.com/joostjager/bitcoin-pa winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" @@ -173,14 +173,14 @@ harness = false #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } # [patch."https://github.com/lightningdevkit/rust-lightning"] -lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } -lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "f5c24c6ba870784b3d038c17ef4e4418d6a24f1d" } +lightning = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-types = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-invoice = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-net-tokio = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-persister = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-background-processor = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-rapid-gossip-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-block-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-transaction-sync = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-liquidity = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } +lightning-macros = { git = "https://github.com/vincenzopalazzo/rust-lightning", rev = "c8363950d753f954b47d2bf4aaff07928c86b085" } diff --git a/src/builder.rs b/src/builder.rs index df54d49fd..0ecc6e686 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -56,14 +56,12 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph, - read_node_metrics, read_output_sweeper, read_payer_proof_contexts, read_payments, - read_peer_info, read_pending_payments, read_scorer, write_node_metrics, + read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments, + read_scorer, write_node_metrics, }; use crate::io::vss_store::VssStoreBuilder; use crate::io::{ - self, PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; @@ -1262,19 +1260,14 @@ fn build_with_store_internal( let kv_store_ref = Arc::clone(&kv_store); let logger_ref = Arc::clone(&logger); - let ( - payment_store_res, - node_metris_res, - pending_payment_store_res, - payer_proof_context_store_res, - ) = runtime.block_on(async move { - tokio::join!( - read_payments(&*kv_store_ref, Arc::clone(&logger_ref)), - read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), - read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)), - read_payer_proof_contexts(&*kv_store_ref, Arc::clone(&logger_ref)) - ) - }); + let (payment_store_res, node_metris_res, pending_payment_store_res) = + runtime.block_on(async move { + tokio::join!( + read_payments(&*kv_store_ref, Arc::clone(&logger_ref)), + read_node_metrics(&*kv_store_ref, Arc::clone(&logger_ref)), + read_pending_payments(&*kv_store_ref, Arc::clone(&logger_ref)), + ) + }); // Initialize the status fields. let node_metrics = match node_metris_res { @@ -1303,19 +1296,7 @@ fn build_with_store_internal( }, }; - let payer_proof_context_store = match payer_proof_context_store_res { - Ok(contexts) => Arc::new(PayerProofContextStore::new( - contexts, - PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), - Arc::clone(&kv_store), - Arc::clone(&logger), - )), - Err(e) => { - log_error!(logger, "Failed to read payer proof contexts from store: {}", e); - return Err(BuildError::ReadFailed); - }, - }; + let payer_proof_context_store = Arc::new(PayerProofContextStore::new()); let (chain_source, chain_tip_opt) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { diff --git a/src/event.rs b/src/event.rs index cbad7ae08..2b5307ded 100644 --- a/src/event.rs +++ b/src/event.rs @@ -15,8 +15,6 @@ use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint}; use lightning::events::bump_transaction::BumpTransactionEvent; -#[cfg(not(feature = "uniffi"))] -use lightning::events::PaidBolt12Invoice; use lightning::events::{ ClosureReason, Event as LdkEvent, FundingInfo, PaymentFailureReason, PaymentPurpose, ReplayEvent, @@ -24,7 +22,9 @@ use lightning::events::{ use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; -use lightning::offers::nonce::Nonce; +#[cfg(not(feature = "uniffi"))] +use lightning::offers::payer_proof::Bolt12InvoiceType; +use lightning::offers::payer_proof::PaidBolt12Invoice; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; use lightning::util::config::{ @@ -41,7 +41,7 @@ use crate::connection::ConnectionManager; use crate::data_store::DataStoreUpdateResult; use crate::fee_estimator::ConfirmationTarget; #[cfg(feature = "uniffi")] -use crate::ffi::PaidBolt12Invoice; +use crate::ffi::PaidBolt12Invoice as FfiPaidBolt12Invoice; use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, @@ -50,7 +50,6 @@ use crate::liquidity::LiquiditySource; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; -use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; @@ -64,6 +63,11 @@ use crate::{ UserChannelId, }; +#[cfg(not(feature = "uniffi"))] +type Bolt12InvoiceInfo = Bolt12InvoiceType; +#[cfg(feature = "uniffi")] +type Bolt12InvoiceInfo = FfiPaidBolt12Invoice; + /// An event emitted by [`Node`], which should be handled by the user. /// /// [`Node`]: [`crate::Node`] @@ -86,17 +90,16 @@ pub enum Event { payment_preimage: Option, /// The total fee which was spent at intermediate hops in this payment. fee_paid_msat: Option, - /// The BOLT12 invoice that was paid. + /// The BOLT12 invoice type that was paid. /// /// This is useful for proof of payment. A third party can verify that the payment was made /// by checking that the `payment_hash` in the invoice matches `sha256(payment_preimage)`. /// /// Will be `None` for non-BOLT12 payments. /// - /// Note that static invoices (indicated by [`PaidBolt12Invoice::StaticInvoice`], used for - /// async payments) do not support proof of payment as the payment hash is not derived - /// from a preimage known only to the recipient. - bolt12_invoice: Option, + /// Note that static invoices (via [`Bolt12InvoiceType::StaticInvoice`], used for + /// async payments) do not support payer proofs. + bolt12_invoice: Option, }, /// A sent payment has failed. PaymentFailed { @@ -560,33 +563,12 @@ where } fn persist_payer_proof_context( - &self, payment_id: PaymentId, bolt12_invoice: &Option, - payment_nonce: Option, + &self, payment_id: PaymentId, paid_invoice: &Option, ) { - #[cfg(not(feature = "uniffi"))] - let invoice = match bolt12_invoice { - Some(PaidBolt12Invoice::Bolt12Invoice(invoice)) => invoice.clone(), - _ => return, - }; - #[cfg(feature = "uniffi")] - let invoice = match bolt12_invoice { - Some(PaidBolt12Invoice::Bolt12(invoice)) => invoice.inner.clone(), - _ => return, - }; - - let nonce = match payment_nonce { - Some(nonce) => nonce, - None => return, - }; - - let context = PayerProofContext { payment_id, invoice, nonce }; - if let Err(e) = self.payer_proof_context_store.insert_or_update(context) { - log_error!( - self.logger, - "Failed to persist payer proof context for {}: {}", - payment_id, - e - ); + if let Some(paid_invoice) = paid_invoice { + if paid_invoice.bolt12_invoice().is_some() { + self.payer_proof_context_store.insert_or_update(payment_id, paid_invoice.clone()); + } } } @@ -1103,7 +1085,6 @@ where payment_hash, fee_paid_msat, bolt12_invoice, - payment_nonce, .. } => { let payment_id = if let Some(id) = payment_id { @@ -1112,16 +1093,29 @@ where debug_assert!(false, "payment_id should always be set."); return Ok(()); }; - let bolt12_invoice = bolt12_invoice.map(Into::into); - self.persist_payer_proof_context(payment_id, &bolt12_invoice, payment_nonce); + self.persist_payer_proof_context(payment_id, &bolt12_invoice); + + #[cfg(not(feature = "uniffi"))] + let bolt12_invoice_info: Option = bolt12_invoice + .as_ref() + .and_then(|p| { + p.bolt12_invoice().map(|i| Bolt12InvoiceType::Bolt12Invoice(i.clone())) + }) + .or_else(|| { + bolt12_invoice.as_ref().and_then(|p| { + p.static_invoice().map(|i| Bolt12InvoiceType::StaticInvoice(i.clone())) + }) + }); + #[cfg(feature = "uniffi")] + let bolt12_invoice_info: Option = bolt12_invoice.map(|p| p.into()); let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), fee_paid_msat: Some(fee_paid_msat), status: Some(PaymentStatus::Succeeded), - bolt12_invoice: Some(bolt12_invoice.clone()), + bolt12_invoice: Some(bolt12_invoice_info.clone()), ..PaymentDetailsUpdate::new(payment_id) }; @@ -1153,7 +1147,7 @@ where payment_hash, payment_preimage: Some(payment_preimage), fee_paid_msat, - bolt12_invoice, + bolt12_invoice: bolt12_invoice_info, }; match self.event_queue.add_event(event).await { diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 70796bd5a..9b2b7ecba 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -23,7 +23,6 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid}; pub use lightning::chain::channelmonitor::BalanceSource; -use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice; pub use lightning::events::{ClosureReason, PaymentFailureReason}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; @@ -31,7 +30,10 @@ pub use lightning::ln::types::ChannelId; use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; pub use lightning::offers::offer::OfferId; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; -use lightning::offers::payer_proof::PayerProof as LdkPayerProof; +use lightning::offers::payer_proof::{ + Bolt12InvoiceType as LdkBolt12InvoiceType, PaidBolt12Invoice as LdkPaidBolt12Invoice, + PayerProof as LdkPayerProof, +}; use lightning::offers::refund::Refund as LdkRefund; use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice; use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName; @@ -837,42 +839,41 @@ pub enum PaidBolt12Invoice { impl From for PaidBolt12Invoice { fn from(ldk: LdkPaidBolt12Invoice) -> Self { - match ldk { - LdkPaidBolt12Invoice::Bolt12Invoice(invoice) => { - PaidBolt12Invoice::Bolt12(Arc::new(Bolt12Invoice::from(invoice))) - }, - LdkPaidBolt12Invoice::StaticInvoice(invoice) => { - PaidBolt12Invoice::Static(Arc::new(StaticInvoice::from(invoice))) - }, + if let Some(invoice) = ldk.bolt12_invoice() { + PaidBolt12Invoice::Bolt12(Arc::new(Bolt12Invoice::from(invoice.clone()))) + } else if let Some(invoice) = ldk.static_invoice() { + PaidBolt12Invoice::Static(Arc::new(StaticInvoice::from(invoice.clone()))) + } else { + panic!("PaidBolt12Invoice must contain either a Bolt12Invoice or StaticInvoice") } } } -impl From for LdkPaidBolt12Invoice { - fn from(wrapper: PaidBolt12Invoice) -> Self { - match wrapper { +impl Writeable for PaidBolt12Invoice { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + let invoice_type = match self { PaidBolt12Invoice::Bolt12(invoice) => { - LdkPaidBolt12Invoice::Bolt12Invoice(invoice.inner.clone()) + LdkBolt12InvoiceType::Bolt12Invoice(invoice.inner.clone()) }, PaidBolt12Invoice::Static(invoice) => { - LdkPaidBolt12Invoice::StaticInvoice(invoice.inner.clone()) + LdkBolt12InvoiceType::StaticInvoice(invoice.inner.clone()) }, - } - } -} - -impl Writeable for PaidBolt12Invoice { - fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { - // TODO: Find way to avoid cloning invoice data. - let ldk_type: LdkPaidBolt12Invoice = self.clone().into(); - ldk_type.write(w) + }; + invoice_type.write(w) } } impl Readable for PaidBolt12Invoice { fn read(r: &mut R) -> Result { - let ldk_type = LdkPaidBolt12Invoice::read(r)?; - Ok(ldk_type.into()) + let invoice_type = LdkBolt12InvoiceType::read(r)?; + Ok(match invoice_type { + LdkBolt12InvoiceType::Bolt12Invoice(invoice) => { + PaidBolt12Invoice::Bolt12(Arc::new(Bolt12Invoice::from(invoice))) + }, + LdkBolt12InvoiceType::StaticInvoice(invoice) => { + PaidBolt12Invoice::Static(Arc::new(StaticInvoice::from(invoice))) + }, + }) } } diff --git a/src/io/mod.rs b/src/io/mod.rs index d5966d61f..e080d39f7 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -82,7 +82,3 @@ pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices /// The pending payment information will be persisted under this prefix. pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments"; pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; - -/// The payer proof context will be persisted under this prefix. -pub(crate) const PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payer_proof_contexts"; -pub(crate) const PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/io/utils.rs b/src/io/utils.rs index 2915f1d57..eef71ec0b 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -42,11 +42,8 @@ use crate::config::WALLET_KEYS_SEED_LEN; use crate::fee_estimator::OnchainFeeEstimator; use crate::io::{ NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::payer_proof_store::PayerProofContext; use crate::payment::PendingPaymentDetails; use crate::peer_store::PeerStore; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; @@ -301,79 +298,6 @@ where Ok(res) } -/// Read previously persisted payer proof contexts from the store. -pub(crate) async fn read_payer_proof_contexts( - kv_store: &DynStore, logger: L, -) -> Result, std::io::Error> -where - L::Target: LdkLogger, -{ - let mut res = Vec::new(); - - let mut stored_keys = KVStore::list( - &*kv_store, - PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, - ) - .await?; - - const BATCH_SIZE: usize = 50; - - let mut set = tokio::task::JoinSet::new(); - - while set.len() < BATCH_SIZE && !stored_keys.is_empty() { - if let Some(next_key) = stored_keys.pop() { - let fut = KVStore::read( - &*kv_store, - PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, - &next_key, - ); - set.spawn(fut); - debug_assert!(set.len() <= BATCH_SIZE); - } - } - - while let Some(read_res) = set.join_next().await { - let reader = read_res - .map_err(|e| { - log_error!(logger, "Failed to read PayerProofContext: {}", e); - set.abort_all(); - e - })? - .map_err(|e| { - log_error!(logger, "Failed to read PayerProofContext: {}", e); - set.abort_all(); - e - })?; - - if let Some(next_key) = stored_keys.pop() { - let fut = KVStore::read( - &*kv_store, - PAYER_PROOF_CONTEXT_PERSISTENCE_PRIMARY_NAMESPACE, - PAYER_PROOF_CONTEXT_PERSISTENCE_SECONDARY_NAMESPACE, - &next_key, - ); - set.spawn(fut); - debug_assert!(set.len() <= BATCH_SIZE); - } - - let context = PayerProofContext::read(&mut &*reader).map_err(|e| { - log_error!(logger, "Failed to deserialize PayerProofContext: {}", e); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to deserialize PayerProofContext", - ) - })?; - res.push(context); - } - - debug_assert!(set.is_empty()); - debug_assert!(stored_keys.is_empty()); - - Ok(res) -} - /// Read `OutputSweeper` state from the store. pub(crate) async fn read_output_sweeper( broadcaster: Arc, fee_estimator: Arc, diff --git a/src/lib.rs b/src/lib.rs index c949100af..8696511f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1780,7 +1780,7 @@ impl Node { /// Remove the payment with the given id from the store. pub fn remove_payment(&self, payment_id: &PaymentId) -> Result<(), Error> { - self.payer_proof_context_store.remove(&payment_id)?; + self.payer_proof_context_store.remove(&payment_id); self.payment_store.remove(&payment_id) } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 074d74aed..594675baf 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -14,12 +14,14 @@ use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::events::PaidBolt12Invoice as LdkPaidBolt12Invoice; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; use lightning::ln::outbound_payment::Retry; use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; use lightning::offers::parse::Bolt12SemanticError; #[cfg(not(feature = "uniffi"))] +use lightning::offers::payer_proof::Bolt12InvoiceType; +use lightning::offers::payer_proof::PaidBolt12Invoice as LdkPaidBolt12Invoice; +#[cfg(not(feature = "uniffi"))] use lightning::offers::payer_proof::PayerProof as LdkPayerProof; use lightning::routing::router::RouteParametersConfig; use lightning::sign::{EntropySource, NodeSigner}; @@ -31,9 +33,11 @@ use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::payer_proof_store::PayerProofContext; -use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::types::{ChannelManager, KeysManager, PayerProofContextStore, PaymentStore}; +use crate::payment::payer_proof_store::PayerProofContextStore; +use crate::payment::store::{ + Bolt12InvoiceInfo, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, +}; +use crate::types::{ChannelManager, KeysManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; @@ -150,8 +154,6 @@ impl Bolt12Payment { payer_note: payer_note.clone(), retry_strategy, route_params_config: route_parameters, - contact_secrets: None, - payer_offer: None, }; let res = if let Some(hrn) = hrn { let hrn = maybe_deref(&hrn); @@ -282,7 +284,7 @@ impl Bolt12Payment { fn payer_proof_context( &self, payment_id: &PaymentId, - ) -> Result<(PaymentDetails, PayerProofContext), Error> { + ) -> Result<(PaymentDetails, LdkPaidBolt12Invoice), Error> { let payment = self.payment_store.get(payment_id).ok_or(Error::PayerProofUnavailable)?; if payment.direction != PaymentDirection::Outbound || payment.status != PaymentStatus::Succeeded @@ -290,9 +292,9 @@ impl Bolt12Payment { return Err(Error::PayerProofUnavailable); } - let context = + let paid_invoice = self.payer_proof_context_store.get(payment_id).ok_or(Error::PayerProofUnavailable)?; - Ok((payment, context)) + Ok((payment, paid_invoice)) } } @@ -338,8 +340,6 @@ impl Bolt12Payment { payer_note: payer_note.clone(), retry_strategy, route_params_config: route_parameters, - contact_secrets: None, - payer_offer: None, }; let res = if let Some(quantity) = quantity { self.channel_manager @@ -443,15 +443,20 @@ impl Bolt12Payment { pub fn create_payer_proof( &self, payment_id: &PaymentId, options: Option, ) -> Result { - let (payment, context) = self.payer_proof_context(payment_id)?; - let preimage = match payment.kind { + let (payment, paid_invoice) = self.payer_proof_context(payment_id)?; + let _preimage = match payment.kind { PaymentKind::Bolt12Offer { preimage: Some(preimage), .. } | PaymentKind::Bolt12Refund { preimage: Some(preimage), .. } => preimage, _ => return Err(Error::PayerProofUnavailable), }; let options = options.unwrap_or_default(); - let mut builder = context.invoice.payer_proof_builder(preimage).map_err(|e| { + let expanded_key = self.keys_manager.get_expanded_key(); + let secp_ctx = bitcoin::secp256k1::Secp256k1::new(); + + let mut builder = paid_invoice + .prove_payer_derived(&expanded_key, *payment_id, &secp_ctx) + .map_err(|e| { log_error!( self.logger, "Failed to initialize payer proof builder for {}: {:?}", @@ -487,18 +492,10 @@ impl Bolt12Payment { builder = builder.include_invoice_created_at(); } - let expanded_key = self.keys_manager.get_expanded_key(); - let proof = builder - .build_with_derived_key( - &expanded_key, - context.nonce, - *payment_id, - options.note.as_deref(), - ) - .map_err(|e| { - log_error!(self.logger, "Failed to build payer proof for {}: {:?}", payment_id, e); - Error::PayerProofCreationFailed - })?; + let proof = builder.build_and_sign(options.note).map_err(|e| { + log_error!(self.logger, "Failed to build payer proof for {}: {:?}", payment_id, e); + Error::PayerProofCreationFailed + })?; Ok(maybe_wrap(proof)) } @@ -558,13 +555,21 @@ impl Bolt12Payment { let payment_hash = invoice.payment_hash(); let payment_id = PaymentId(payment_hash.0); + #[cfg(not(feature = "uniffi"))] + let bolt12_invoice_info: Option = + Some(Bolt12InvoiceType::Bolt12Invoice(invoice.clone())); + #[cfg(feature = "uniffi")] + let bolt12_invoice_info: Option = Some(crate::ffi::PaidBolt12Invoice::Bolt12( + std::sync::Arc::new(crate::ffi::Bolt12Invoice::from(invoice.clone())), + )); + let kind = PaymentKind::Bolt12Refund { hash: Some(payment_hash), preimage: None, secret: None, payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), quantity: refund.quantity(), - bolt12_invoice: Some(LdkPaidBolt12Invoice::Bolt12Invoice(invoice.clone()).into()), + bolt12_invoice: bolt12_invoice_info, }; let payment = PaymentDetails::new( diff --git a/src/payment/payer_proof_store.rs b/src/payment/payer_proof_store.rs index 2ab36633b..8c43d84b4 100644 --- a/src/payment/payer_proof_store.rs +++ b/src/payment/payer_proof_store.rs @@ -1,87 +1,45 @@ // This file is Copyright its original authors, visible in version control history. // // This file is licensed under the Apache License, Version 2.0 or the MIT license or the MIT license , at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::impl_writeable_tlv_based; -use lightning::ln::channelmanager::PaymentId; -use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::nonce::Nonce; - -use crate::data_store::{StorableObject, StorableObjectUpdate}; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct PayerProofContext { - pub payment_id: PaymentId, - pub invoice: Bolt12Invoice, - pub nonce: Nonce, -} +use std::collections::HashMap; +use std::sync::Mutex; -impl_writeable_tlv_based!(PayerProofContext, { - (0, payment_id, required), - (2, invoice, required), - (4, nonce, required), -}); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct PayerProofContextUpdate { - pub payment_id: PaymentId, - pub invoice: Option, - pub nonce: Option, -} - -impl From<&PayerProofContext> for PayerProofContextUpdate { - fn from(value: &PayerProofContext) -> Self { - Self { - payment_id: value.payment_id, - invoice: Some(value.invoice.clone()), - nonce: Some(value.nonce), - } - } +use lightning::ln::channelmanager::PaymentId; +use lightning::offers::payer_proof::PaidBolt12Invoice; + +/// In-memory store for payer proof contexts, keyed by payment ID. +/// +/// Stores the [`PaidBolt12Invoice`] received in [`Event::PaymentSent`] so that +/// payer proofs can be constructed later via [`Bolt12Payment::create_payer_proof`]. +/// +/// Note: This store is not persisted to disk. Payer proof contexts are lost on +/// restart. This is a known limitation of the current upstream LDK API where +/// [`PaidBolt12Invoice`] cannot be serialized and reconstructed externally. +/// +/// [`Event::PaymentSent`]: lightning::events::Event::PaymentSent +/// [`Bolt12Payment::create_payer_proof`]: crate::payment::bolt12::Bolt12Payment::create_payer_proof +pub(crate) struct PayerProofContextStore { + entries: Mutex>, } -impl StorableObject for PayerProofContext { - type Id = PaymentId; - type Update = PayerProofContextUpdate; - - fn id(&self) -> Self::Id { - self.payment_id +impl PayerProofContextStore { + pub fn new() -> Self { + Self { entries: Mutex::new(HashMap::new()) } } - fn update(&mut self, update: Self::Update) -> bool { - debug_assert_eq!( - self.payment_id, update.payment_id, - "We should only ever override payer proof context for the same payment id" - ); - - let mut updated = false; - - if let Some(invoice) = update.invoice { - if self.invoice != invoice { - self.invoice = invoice; - updated = true; - } - } - - if let Some(nonce) = update.nonce { - if self.nonce != nonce { - self.nonce = nonce; - updated = true; - } - } - - updated + pub fn insert_or_update(&self, payment_id: PaymentId, paid_invoice: PaidBolt12Invoice) { + self.entries.lock().unwrap().insert(payment_id, paid_invoice); } - fn to_update(&self) -> Self::Update { - self.into() + pub fn get(&self, payment_id: &PaymentId) -> Option { + self.entries.lock().unwrap().get(payment_id).cloned() } -} -impl StorableObjectUpdate for PayerProofContextUpdate { - fn id(&self) -> ::Id { - self.payment_id + pub fn remove(&self, payment_id: &PaymentId) { + self.entries.lock().unwrap().remove(payment_id); } } diff --git a/src/payment/store.rs b/src/payment/store.rs index a9585e9c3..9179b07dd 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -8,11 +8,11 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use bitcoin::{BlockHash, Txid}; -#[cfg(not(feature = "uniffi"))] -use lightning::events::PaidBolt12Invoice; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; +#[cfg(not(feature = "uniffi"))] +use lightning::offers::payer_proof::Bolt12InvoiceType; use lightning::util::ser::{Readable, Writeable}; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, @@ -26,6 +26,11 @@ use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; use crate::ffi::PaidBolt12Invoice; use crate::hex_utils; +#[cfg(not(feature = "uniffi"))] +pub(crate) type Bolt12InvoiceInfo = Bolt12InvoiceType; +#[cfg(feature = "uniffi")] +pub(crate) type Bolt12InvoiceInfo = PaidBolt12Invoice; + /// Represents a payment. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -445,7 +450,7 @@ pub enum PaymentKind { /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, /// The BOLT12 invoice associated with the payment, once available. - bolt12_invoice: Option, + bolt12_invoice: Option, }, /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. /// @@ -467,7 +472,7 @@ pub enum PaymentKind { /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, /// The BOLT12 invoice associated with the payment, once available. - bolt12_invoice: Option, + bolt12_invoice: Option, }, /// A spontaneous ("keysend") payment. Spontaneous { @@ -577,7 +582,7 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, - pub bolt12_invoice: Option>, + pub bolt12_invoice: Option>, pub txid: Option, } diff --git a/src/types.rs b/src/types.rs index 04650cfc6..b92116cea 100644 --- a/src/types.rs +++ b/src/types.rs @@ -37,7 +37,7 @@ use crate::data_store::DataStore; use crate::fee_estimator::OnchainFeeEstimator; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::payer_proof_store::PayerProofContext; +pub(crate) use crate::payment::payer_proof_store::PayerProofContextStore; use crate::payment::{PaymentDetails, PendingPaymentDetails}; use crate::runtime::RuntimeSpawner; @@ -350,7 +350,6 @@ pub(crate) type BumpTransactionEventHandler = >; pub(crate) type PaymentStore = DataStore>; -pub(crate) type PayerProofContextStore = DataStore>; /// A local, potentially user-provided, identifier of a channel. ///