diff --git a/src/event.rs b/src/event.rs index 22848bec1..1b9ef8fee 100644 --- a/src/event.rs +++ b/src/event.rs @@ -745,6 +745,7 @@ where offer_id, payer_note, quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 052571818..dd274e185 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -180,6 +180,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + payment_metadata_id: None, }; let payment = PaymentDetails::new( payment_id, @@ -203,6 +204,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + payment_metadata_id: None, }; let payment = PaymentDetails::new( payment_id, @@ -315,6 +317,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: Some(*payment_secret), + payment_metadata_id: None, }; let payment = PaymentDetails::new( @@ -339,6 +342,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: Some(*payment_secret), + payment_metadata_id: None, }; let payment = PaymentDetails::new( payment_id, @@ -573,6 +577,7 @@ impl Bolt11Payment { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), + payment_metadata_id: None, }; let payment = PaymentDetails::new( id, diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 8006f4bb9..f1c352510 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -106,6 +106,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( payment_id, @@ -131,6 +132,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( payment_id, @@ -212,6 +214,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( payment_id, @@ -237,6 +240,7 @@ impl Bolt12Payment { offer_id: offer.id(), payer_note: payer_note.map(UntrustedString), quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( payment_id, @@ -334,6 +338,7 @@ impl Bolt12Payment { secret: None, payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())), quantity: refund.quantity(), + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( @@ -400,6 +405,7 @@ impl Bolt12Payment { secret: None, payer_note: payer_note.map(|note| UntrustedString(note)), quantity, + payment_metadata_ids: Vec::new(), }; let payment = PaymentDetails::new( payment_id, diff --git a/src/payment/store.rs b/src/payment/store.rs index 75b2b1b2a..d9afef3eb 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -8,7 +8,9 @@ use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; -use lightning::util::ser::{Readable, Writeable}; +use lightning::offers::refund::Refund; +use lightning::onion_message::dns_resolution::{DNSSECProof, HumanReadableName}; +use lightning::util::ser::{Hostname, Readable, Writeable, Writer}; use lightning::util::string::UntrustedString; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, @@ -18,6 +20,7 @@ use lightning::{ use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use bitcoin::{BlockHash, Txid}; +use rand::RngCore; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -140,7 +143,7 @@ impl Readable for PaymentDetails { lsp_fee_limits, } } else { - PaymentKind::Bolt11 { hash, preimage, secret } + PaymentKind::Bolt11 { hash, preimage, secret, payment_metadata_id: None } } } else { PaymentKind::Spontaneous { hash, preimage } @@ -178,6 +181,11 @@ impl StorableObject for PaymentDetails { "We should only ever override payment data for the same payment id" ); + debug_assert!( + update.payment_metadata_id.is_none() || update.list_payment_metadata_id.is_none(), + "Exactly one of `payment_metadata_id` or `list_payment_metadata_id` must be present, not both or neither." + ); + let mut updated = false; macro_rules! update_if_necessary { @@ -293,6 +301,43 @@ impl StorableObject for PaymentDetails { } } + if let Some(metadata_id) = update.payment_metadata_id { + match self.kind { + PaymentKind::Bolt11 { ref mut payment_metadata_id, .. } => { + update_if_necessary!(*payment_metadata_id, metadata_id); + }, + PaymentKind::Bolt12Offer { ref mut payment_metadata_ids, .. } => { + if let Some(metadata_id_unwrapped) = metadata_id { + if !payment_metadata_ids.contains(&metadata_id_unwrapped) { + payment_metadata_ids.push(metadata_id_unwrapped); + updated = true; + } + } + }, + PaymentKind::Bolt12Refund { ref mut payment_metadata_ids, .. } => { + if let Some(metadata_id_unwrapped) = metadata_id { + if !payment_metadata_ids.contains(&metadata_id_unwrapped) { + payment_metadata_ids.push(metadata_id_unwrapped); + updated = true; + } + } + }, + _ => {}, + } + } + + if let Some(list_payment_metadata_id) = &update.list_payment_metadata_id { + match self.kind { + PaymentKind::Bolt12Offer { ref mut payment_metadata_ids, .. } => { + update_if_necessary!(*payment_metadata_ids, list_payment_metadata_id.clone()); + }, + PaymentKind::Bolt12Refund { ref mut payment_metadata_ids, .. } => { + update_if_necessary!(*payment_metadata_ids, list_payment_metadata_id.clone()); + }, + _ => {}, + } + } + if updated { self.latest_update_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -364,6 +409,8 @@ pub enum PaymentKind { preimage: Option, /// The secret used by the payment. secret: Option, + /// The id of the payment metadata + payment_metadata_id: Option, }, /// A [BOLT 11] payment intended to open an [bLIP-52 / LSPS 2] just-in-time channel. /// @@ -417,6 +464,9 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + + /// The ids of the payment metadata + payment_metadata_ids: Vec, }, /// A [BOLT 12] 'refund' payment, i.e., a payment for a [`Refund`]. /// @@ -437,6 +487,9 @@ pub enum PaymentKind { /// /// This will always be `None` for payments serialized with version `v0.3.0`. quantity: Option, + + /// The ids of the payment metadata + payment_metadata_ids: Vec, }, /// A spontaneous ("keysend") payment. Spontaneous { @@ -456,6 +509,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (0, hash, required), (2, preimage, option), (4, secret, option), + (5, payment_metadata_id, option), }, (4, Bolt11Jit) => { (0, hash, required), @@ -471,6 +525,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (3, quantity, option), (4, secret, option), (6, offer_id, required), + (7, payment_metadata_ids, optional_vec), }, (8, Spontaneous) => { (0, hash, required), @@ -482,6 +537,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, (2, preimage, option), (3, quantity, option), (4, secret, option), + (5, payment_metadata_ids, optional_vec), } ); @@ -542,6 +598,8 @@ pub(crate) struct PaymentDetailsUpdate { pub direction: Option, pub status: Option, pub confirmation_status: Option, + pub payment_metadata_id: Option>, + pub list_payment_metadata_id: Option>, } impl PaymentDetailsUpdate { @@ -557,19 +615,39 @@ impl PaymentDetailsUpdate { direction: None, status: None, confirmation_status: None, + payment_metadata_id: None, + list_payment_metadata_id: None, } } } 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, payment_metadata_id) = match value.kind { + PaymentKind::Bolt11 { hash, preimage, secret, payment_metadata_id, .. } => { + (Some(hash), preimage, secret, payment_metadata_id) + }, + PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => { + (Some(hash), preimage, secret, None) + }, + PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => { + (hash, preimage, secret, None) + }, + PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { + (hash, preimage, secret, None) + }, + PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None, None), + _ => (None, None, None, None), + }; + + let list_payment_metadata_id = match &value.kind { + PaymentKind::Bolt12Offer { payment_metadata_ids, .. } => { + Some(payment_metadata_ids.clone()) + }, + PaymentKind::Bolt12Refund { payment_metadata_ids, .. } => { + Some(payment_metadata_ids.clone()) + }, + _ => None, }; let confirmation_status = match value.kind { @@ -595,6 +673,8 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { direction: Some(value.direction), status: Some(value.status), confirmation_status, + payment_metadata_id: Some(payment_metadata_id), + list_payment_metadata_id, } } } @@ -605,6 +685,383 @@ impl StorableObjectUpdate for PaymentDetailsUpdate { } } +#[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] +pub struct PaymentMetadataId(pub [u8; Self::LENGTH]); + +impl PaymentMetadataId { + pub const LENGTH: usize = 16; + pub fn new() -> Self { + let mut random_bytes = [0u8; Self::LENGTH]; + rand::thread_rng().fill_bytes(&mut random_bytes); + let mut id = [0u8; 16]; + for byte in id.iter_mut() { + *byte = rand::random::(); + } + PaymentMetadataId(id) + } +} + +impl Writeable for PaymentMetadataId { + fn write( + &self, w: &mut W, + ) -> Result<(), lightning::io::Error> { + self.0.write(w) + } +} + +impl Readable for PaymentMetadataId { + fn read(r: &mut R) -> Result { + let buf: [u8; 16] = Readable::read(r)?; + Ok(PaymentMetadataId(buf)) + } +} + +#[derive(Hash, Clone, PartialEq, Eq, Debug)] +/// Wrapper type for Vec to implement Writeable. +pub struct PaymentMetadataIdList(pub Vec); + +impl Writeable for PaymentMetadataIdList { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + writer.write_all(&(self.0.len() as u64).to_le_bytes())?; + for id in &self.0 { + id.write(writer)?; + } + Ok(()) + } +} + +impl Readable for PaymentMetadataIdList { + fn read(reader: &mut R) -> Result { + let len = u64::read(reader)? as usize; + let mut ids = Vec::with_capacity(len); + for _ in 0..len { + ids.push(PaymentMetadataId::read(reader)?); + } + Ok(PaymentMetadataIdList(ids)) + } +} + +impl StorableObjectId for PaymentMetadataId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.0) + } +} + +/// Represents a payment metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentMetadata { + /// The identifier of this payment. + pub id: PaymentMetadataId, + /// The timestamp, in seconds since start of the UNIX epoch, when this entry was last updated. + pub latest_update_timestamp: u64, + /// The metadata detail of the payment. + /// + /// This can be a BOLT 11 invoice, a BOLT 12 offer, or a BOLT 12 refund. + pub payment_metadata_detail: PaymentMetadataDetail, + /// The limits applying to how much fee we allow an LSP to deduct from the payment amount. + pub jit_channel_fee_limit: Option, +} + +impl PaymentMetadata { + pub(crate) fn new( + id: Option, payment_metadata_detail: PaymentMetadataDetail, + jit_channel_fee_limit: Option, + ) -> Self { + let latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + + let payment_metadata_id = if let Some(id) = id { id } else { PaymentMetadataId::new() }; + + PaymentMetadata { + id: payment_metadata_id, + latest_update_timestamp, + payment_metadata_detail, + jit_channel_fee_limit, + } + } + + pub(crate) fn update(&mut self, update: &PaymentMetadataUpdate) -> bool { + debug_assert_eq!( + self.id, update.id, + "We should only ever override payment metadata data for the same payment id" + ); + + let mut updated = false; + + macro_rules! update_if_necessary { + ($val: expr, $update: expr) => { + if $val != $update { + $val = $update; + updated = true; + } + }; + } + + if let Some(hrn_opt) = update.hrn.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + _ => {}, + } + } + + if let Some(dnssec_proof_opt) = update.dnssec_proof.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + _ => {}, + } + } + if updated { + self.latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + } + + updated + } +} + +impl Writeable for PaymentMetadata { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), + (2, self.latest_update_timestamp, required), + (4, self.payment_metadata_detail, required), + (6, self.jit_channel_fee_limit, option), + }); + Ok(()) + } +} + +impl Readable for PaymentMetadata { + fn read(reader: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), + (2, latest_update_timestamp, required), + (4, payment_metadata_detail, required), + (6, jit_channel_fee_limit, option), + }); + + Ok(PaymentMetadata { + id: id.0.ok_or(DecodeError::InvalidValue)?, + latest_update_timestamp: latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?, + payment_metadata_detail: payment_metadata_detail.0.ok_or(DecodeError::InvalidValue)?, + jit_channel_fee_limit, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Defines fee limits for Just-In-Time (JIT) channels opened by a Lightning Service Provider (LSP) +/// to provide inbound liquidity, as per the LSPS2 protocol (bLIP-52). Used in `PaymentMetadata` to +/// track and constrain costs associated with dynamically opened channels for payments. +pub struct JitChannelFeeLimits { + /// The maximal total amount we allow any configured LSP withhold from us when forwarding the + /// payment. + pub max_total_opening_fee_msat: Option, + /// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured + /// LSP withhold from us when forwarding the payment. + pub max_proportional_opening_fee_ppm_msat: Option, + /// The minimum payment amount (in millisatoshis) allowed by the LSP + pub min_payment_size_msat: Option, + /// The maximum payment amount (in millisatoshis) allowed by the LSP + pub max_payment_size_msat: Option, +} +impl_writeable_tlv_based!(JitChannelFeeLimits, { + (0, max_total_opening_fee_msat, option), + (2, max_proportional_opening_fee_ppm_msat, option), + (4, min_payment_size_msat, option), + (6, max_payment_size_msat, option), +}); + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Represents the metadata details of a payment. +/// +/// This enum encapsulates various types of payment metadata, such as BOLT 11 invoices, +/// BOLT 12 offers, and BOLT 12 refunds, along with their associated details. +pub enum PaymentMetadataDetail { + /// A [BOLT 11] metadata. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + Bolt11 { + /// The invoice associated with the payment. + invoice: String, + }, + /// A [BOLT 12] 'offer' payment metadata, i.e., a payment metadata for an [`Offer`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Offer`]: crate::lightning::offers::offer::Offer + Bolt12Offer { + /// The DNSSEC proof associated with the payment. + dnssec_proof: Option, + /// The human-readable name associated with the payment. + hrn: Option, + }, + /// A [BOLT 12] 'refund' payment metadata, i.e., a payment metadata for a [`Refund`]. + /// + /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + /// [`Refund`]: lightning::offers::refund::Refund + Bolt12Refund { + /// The human-readable name associated with the refund payment. + hrn: Option, + /// The DNSSEC proof associated with the refund payment. + dnssec_proof: Option, + /// The invoice associated with the refund payment. + invoice: Vec, + }, +} +impl_writeable_tlv_based_enum! { PaymentMetadataDetail, + (0, Bolt11) => { + (0, invoice, required), + }, + (4, Bolt12Offer) => { + (0, dnssec_proof, option), + (2, hrn, option), + }, + (6, Bolt12Refund) => { + (0, invoice, required), + (2, hrn, option), + (4, dnssec_proof, option), + } +} +#[derive(Clone, Debug, PartialEq, Eq)] +/// A wrapper for `DNSSECProof` to enable serialization and deserialization, +/// allowing it to be stored in the payment store. +pub struct DNSSECProofWrapper(pub DNSSECProof); + +impl Writeable for DNSSECProofWrapper { + fn write(&self, w: &mut W) -> Result<(), lightning::io::Error> { + (self.0.name.as_str().len() as u8).write(w)?; + w.write_all(&self.0.name.as_str().as_bytes())?; + self.0.proof.write(w)?; + + Ok(()) + } +} + +impl Readable for DNSSECProofWrapper { + fn read(r: &mut R) -> Result { + let s = Hostname::read(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + let proof = Vec::::read(r)?; + + Ok(DNSSECProofWrapper(DNSSECProof { name, proof })) + } +} + +impl StorableObject for PaymentMetadata { + type Id = PaymentMetadataId; + type Update = PaymentMetadataUpdate; + fn id(&self) -> Self::Id { + self.id + } + fn update(&mut self, update: &Self::Update) -> bool { + debug_assert_eq!( + self.id, update.id, + "We should only ever override payment metadata data for the same payment id" + ); + + let mut updated = false; + + macro_rules! update_if_necessary { + ($val: expr, $update: expr) => { + if $val != $update { + $val = $update; + updated = true; + } + }; + } + + if let Some(hrn_opt) = update.hrn.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut hrn, .. } => { + update_if_necessary!(*hrn, Some(hrn_opt.clone())); + }, + _ => {}, + } + } + + if let Some(dnssec_proof_opt) = update.dnssec_proof.clone() { + match self.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + PaymentMetadataDetail::Bolt12Refund { ref mut dnssec_proof, .. } => { + update_if_necessary!(*dnssec_proof, Some(dnssec_proof_opt.clone())); + }, + _ => {}, + } + } + + if updated { + self.latest_update_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + } + + updated + } + + fn to_update(&self) -> Self::Update { + self.into() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PaymentMetadataUpdate { + pub id: PaymentMetadataId, + pub hrn: Option, // Hashed Recipient Name + pub dnssec_proof: Option, // Prova DNSSEC +} + +impl PaymentMetadataUpdate { + pub fn new(id: PaymentMetadataId) -> Self { + Self { id, hrn: None, dnssec_proof: None } + } +} + +impl From<&PaymentMetadata> for PaymentMetadataUpdate { + fn from(value: &PaymentMetadata) -> Self { + let (hrn, dnssec_proof) = match &value.payment_metadata_detail { + PaymentMetadataDetail::Bolt12Offer { hrn, dnssec_proof, .. } => { + (hrn.clone(), dnssec_proof.clone()) + }, + PaymentMetadataDetail::Bolt12Refund { hrn, dnssec_proof, .. } => { + (hrn.clone(), dnssec_proof.clone()) + }, + _ => (None, None), + }; + + Self { id: value.id, hrn, dnssec_proof } + } +} + +impl StorableObjectUpdate for PaymentMetadataUpdate { + fn id(&self) -> ::Id { + self.id + } +} + #[cfg(test)] mod tests { use super::*; @@ -670,7 +1127,12 @@ mod tests { ); match bolt11_decoded.kind { - PaymentKind::Bolt11 { hash: h, preimage: p, secret: s } => { + PaymentKind::Bolt11 { + hash: h, + preimage: p, + secret: s, + payment_metadata_id: None, + } => { assert_eq!(hash, h); assert_eq!(preimage, p); assert_eq!(secret, s); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index db48eca23..81a0ff262 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -817,6 +817,7 @@ fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + payment_metadata_ids: _, } => { assert!(hash.is_some()); assert!(preimage.is_some()); @@ -883,6 +884,7 @@ fn simple_bolt12_send_receive() { offer_id, quantity: ref qty, payer_note: ref note, + payment_metadata_ids: _, } => { assert!(hash.is_some()); assert!(preimage.is_some()); @@ -950,6 +952,7 @@ fn simple_bolt12_send_receive() { secret: _, quantity: ref qty, payer_note: ref note, + payment_metadata_ids: _, } => { assert!(hash.is_some()); assert!(preimage.is_some());