Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 162
Add stateless BOLT 12 payer proof support#1045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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::channel_state::{ChannelShutdownState, CounterpartyForwardingInfo}; | ||
| use lightning::ln::channelmanager::PaymentId; | ||
| @@ -32,6 +31,9 @@ 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::{ | ||
| 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; | ||
| @@ -840,6 +842,17 @@ pub enum PaidBolt12Invoice { | ||
| Static(Arc<StaticInvoice>), | ||
| } | ||
| impl PaidBolt12Invoice { | ||
| /// Returns the [`Bolt12Invoice`] if the payment was for a standard BOLT 12 invoice, and | ||
| /// `None` for a static invoice, i.e., an async payment, which can't be proven. | ||
| pub fn bolt12_invoice(&self) -> Option<Arc<Bolt12Invoice>> { | ||
| match self { | ||
| PaidBolt12Invoice::Bolt12(invoice) => Some(Arc::clone(invoice)), | ||
| PaidBolt12Invoice::Static(_) => None, | ||
| } | ||
| } | ||
| } | ||
| impl From<LdkPaidBolt12Invoice> for PaidBolt12Invoice { | ||
| fn from(ldk: LdkPaidBolt12Invoice) -> Self { | ||
| match ldk { | ||
| @@ -881,6 +894,135 @@ impl Readable for PaidBolt12Invoice { | ||
| } | ||
| } | ||
| /// A cryptographic proof that a BOLT12 invoice was paid by this node. | ||
| /// | ||
| /// Hand the encoded form, via [`Self::bytes`] or [`Self::as_string`], to whoever needs to verify | ||
| /// it. The remaining accessors expose the fields that were selectively disclosed when the proof | ||
| /// was created. | ||
| #[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<u8>) -> Result<Self, Error> { | ||
| let inner = LdkPayerProof::try_from(proof_bytes).map_err(|_| Error::InvalidPayerProof)?; | ||
| Ok(Self { inner }) | ||
| } | ||
| /// Parses a payer proof from its bech32-encoded string form, as returned by | ||
| /// [`Self::as_string`]. | ||
| #[uniffi::constructor] | ||
| pub fn from_str(proof_str: &str) -> Result<Self, Error> { | ||
| proof_str.parse() | ||
| } | ||
| /// The payment preimage proving the payment completed. | ||
| pub fn payment_preimage(&self) -> PaymentPreimage { | ||
| self.inner.payment_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_signing_pubkey(&self) -> PublicKey { | ||
| self.inner.payer_signing_pubkey() | ||
| } | ||
| /// 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<u8> { | ||
| self.inner.invoice_signature().as_ref().to_vec() | ||
| } | ||
| /// The proof signature bytes. | ||
| pub fn proof_signature(&self) -> Vec<u8> { | ||
| self.inner.proof_signature().as_ref().to_vec() | ||
| } | ||
| /// The offer description, if it was disclosed in the proof. | ||
| pub fn offer_description(&self) -> Option<String> { | ||
| self.inner.offer_description().map(|value| value.to_string()) | ||
| } | ||
| /// The offer issuer, if it was disclosed in the proof. | ||
| pub fn offer_issuer(&self) -> Option<String> { | ||
| self.inner.offer_issuer().map(|value| value.to_string()) | ||
| } | ||
| /// The invoice amount in millisatoshis, if it was disclosed in the proof. | ||
| pub fn invoice_amount_msats(&self) -> Option<u64> { | ||
| self.inner.invoice_amount_msats() | ||
| } | ||
| /// The invoice creation time, in seconds since the UNIX epoch, if it was disclosed in the | ||
| /// proof. | ||
| pub fn invoice_created_at(&self) -> Option<u64> { | ||
| self.inner.invoice_created_at().map(|value| value.as_secs()) | ||
| } | ||
| /// The optional note attached to the proof. | ||
| pub fn proof_note(&self) -> Option<String> { | ||
| self.inner.proof_note().map(|value| value.to_string()) | ||
| } | ||
| /// The Merkle root committed to by the proof. | ||
| pub fn merkle_root(&self) -> Vec<u8> { | ||
| self.inner.merkle_root().to_byte_array().to_vec() | ||
| } | ||
| /// The raw TLV bytes of the proof. | ||
| pub fn bytes(&self) -> Vec<u8> { | ||
| self.inner.bytes().to_vec() | ||
| } | ||
| /// The bech32-encoded string form of the proof. | ||
vincenzopalazzo marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| pub fn as_string(&self) -> String { | ||
| self.inner.to_string() | ||
| } | ||
| } | ||
| impl From<LdkPayerProof> for PayerProof { | ||
| fn from(inner: LdkPayerProof) -> Self { | ||
| Self { inner } | ||
| } | ||
| } | ||
| impl std::str::FromStr for PayerProof { | ||
| type Err = Error; | ||
| fn from_str(proof_str: &str) -> Result<Self, Self::Err> { | ||
| proof_str | ||
| .parse::<LdkPayerProof>() | ||
| .map(|proof| PayerProof { inner: proof }) | ||
| .map_err(|_| Error::InvalidPayerProof) | ||
| } | ||
| } | ||
| 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| { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -18,10 +18,14 @@ 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; | ||
| 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; | ||
| use lightning::sign::{EntropySource, NodeSigner}; | ||
| #[cfg(feature = "uniffi")] | ||
| use lightning::util::ser::{Readable, Writeable}; | ||
| use lightning_types::payment::PaymentPreimage; | ||
| use lightning_types::string::UntrustedString; | ||
| use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; | ||
| @@ -52,6 +56,36 @@ type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadable | ||
| #[cfg(feature = "uniffi")] | ||
| type HumanReadableName = Arc<crate::ffi::HumanReadableName>; | ||
| #[cfg(not(feature = "uniffi"))] | ||
| type PayerProof = LdkPayerProof; | ||
| #[cfg(feature = "uniffi")] | ||
| type PayerProof = Arc<crate::ffi::PayerProof>; | ||
| /// Options controlling which optional fields are disclosed in a [BOLT 12] payer proof. | ||
| /// | ||
| /// A payer proof always commits to the payer id, the payment hash, and the issuer signing | ||
| /// pubkey, and additionally discloses the invoice features whenever the invoice carries any. | ||
| /// Everything else is disclosed only if requested here, allowing to reveal just as much of the | ||
| /// invoice as the verifier needs to see. | ||
| /// | ||
| /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md | ||
| #[derive(Clone, Debug, PartialEq, Eq, Default)] | ||
| #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] | ||
| pub struct PayerProofOptions { | ||
| /// An optional note to attach to the payer proof itself. | ||
| pub note: Option<String>, | ||
| /// Whether to disclose the offer description. | ||
| pub include_offer_description: bool, | ||
| /// Whether to disclose the offer issuer. | ||
vincenzopalazzo marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| pub include_offer_issuer: bool, | ||
| /// Whether to disclose the invoice amount. | ||
| pub include_invoice_amount: bool, | ||
| /// Whether to disclose the invoice creation timestamp. | ||
| pub include_invoice_created_at: bool, | ||
| /// Additional TLV types to disclose, for fields not covered by the flags above. | ||
| pub extra_tlv_types: Vec<u64>, | ||
| } | ||
| /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. | ||
| /// | ||
| /// Should be retrieved by calling [`Node::bolt12_payment`]. | ||
| @@ -389,6 +423,87 @@ impl Bolt12Payment { | ||
| Ok(payment_id) | ||
| } | ||
| /// Creates a [BOLT 12] payer proof for a payment this node made. | ||
| /// | ||
| /// A payer proof lets the payer demonstrate to a third party that they paid a particular | ||
| /// [BOLT 12] invoice, disclosing only the invoice fields they choose to reveal via | ||
| /// [`PayerProofOptions`]. | ||
| /// | ||
| /// All inputs are taken straight from [`Event::PaymentSuccessful`]: pass its `payment_id` and | ||
| /// `payment_preimage`, plus the [`Bolt12Invoice`] out of its `bolt12_invoice` field. Nothing | ||
| /// is read from or written to the payment store, so it's up to you to hold on to the invoice | ||
| /// if you want to build a proof later on. | ||
| /// | ||
| /// Note that payments settled via a static invoice, i.e., async payments, can't be proven this | ||
| /// way, which is why this takes a [`Bolt12Invoice`] rather than the event's | ||
| /// [`PaidBolt12Invoice`]: those payments simply won't yield one. | ||
| /// | ||
| /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md | ||
| /// [`Event::PaymentSuccessful`]: crate::Event::PaymentSuccessful | ||
| /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice | ||
| /// [`PaidBolt12Invoice`]: lightning::offers::payer_proof::PaidBolt12Invoice | ||
| pub fn create_payer_proof( | ||
| &self, payment_id: PaymentId, payment_preimage: PaymentPreimage, invoice: &Bolt12Invoice, | ||
| options: Option<PayerProofOptions>, | ||
| ) -> Result<PayerProof, Error> { | ||
| let invoice = maybe_deref(invoice); | ||
| let paid_invoice = LdkPaidBolt12Invoice::Bolt12Invoice(invoice.clone()); | ||
| let options = options.unwrap_or_default(); | ||
| let expanded_key = self.keys_manager.get_expanded_key(); | ||
| let secp_ctx = bitcoin::secp256k1::Secp256k1::new(); | ||
| let mut builder = paid_invoice | ||
| .prove_payer_derived(payment_preimage, &expanded_key, payment_id, &secp_ctx) | ||
| .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(); | ||
| } | ||
| if let Some(note) = options.note { | ||
| builder = builder.with_proof_note(note); | ||
| } | ||
| let proof = builder.build_and_sign().map_err(|e| { | ||
| log_error!(self.logger, "Failed to build payer proof for {}: {:?}", payment_id, e); | ||
| Error::PayerProofCreationFailed | ||
| })?; | ||
| log_info!(self.logger, "Created payer proof for payment {}", payment_id); | ||
| 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( | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.