Skip to content

Commit 4bbab5d

Browse files
blip42: Add contact secret and payer offer support to invoice requests
Implements BLIP-42 contact management for the sender side: - Add contact_secret and payer_offer fields to InvoiceRequestContents - Add builder methods: contact_secrets(), payer_offer() - Add accessor methods: contact_secret(), payer_offer() - Add OptionalOfferPaymentParams fields for contact_secrects and payer_offer - Update ChannelManager::pay_for_offer to pass contact information - Add create_compact_offer_builder to OffersMessageFlow for small payer offers - Update tests to include new InvoiceRequestFields Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
1 parent 3308248 commit 4bbab5d

File tree

5 files changed

+189
-6
lines changed

5 files changed

+189
-6
lines changed

fuzz/src/invoice_request_deser.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
9898
.payer_note()
9999
.map(|s| UntrustedString(s.to_string())),
100100
human_readable_name: None,
101+
contact_secret: None,
102+
payer_offer: None,
101103
}
102104
};
103105

lightning/src/ln/channelmanager.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ use crate::ln::outbound_payment::{
9090
};
9191
use crate::ln::types::ChannelId;
9292
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
93+
use crate::offers::contacts::ContactSecrets;
9394
use crate::offers::flow::{HeldHtlcReplyPath, InvreqResponseInstructions, OffersMessageFlow};
9495
use crate::offers::invoice::{Bolt12Invoice, UnsignedBolt12Invoice};
9596
use crate::offers::invoice_error::InvoiceError;
@@ -698,6 +699,34 @@ pub struct OptionalOfferPaymentParams {
698699
/// will ultimately fail once all pending paths have failed (generating an
699700
/// [`Event::PaymentFailed`]).
700701
pub retry_strategy: Retry,
702+
/// Contact secrets to include in the invoice request for BLIP-42 contact management.
703+
/// If provided, these secrets will be used to establish a contact relationship with the recipient.
704+
pub contact_secrects: Option<ContactSecrets>,
705+
/// A custom payer offer to include in the invoice request for BLIP-42 contact management.
706+
///
707+
/// If provided, this offer will be included in the invoice request, allowing the recipient to
708+
/// contact you back. If `None`, **no payer offer will be included** in the invoice request.
709+
///
710+
/// You can create custom offers using [`OffersMessageFlow::create_compact_offer_builder`]:
711+
/// - Pass `None` for no blinded path (smallest size, ~70 bytes)
712+
/// - Pass `Some(intro_node_id)` for a single blinded path (~200 bytes)
713+
///
714+
/// # Example
715+
/// ```rust,ignore
716+
/// // Include a compact offer with a single blinded path
717+
/// let payer_offer = flow.create_compact_offer_builder(
718+
/// &entropy_source,
719+
/// Some(trusted_peer_pubkey)
720+
/// )?.build()?;
721+
///
722+
/// let params = OptionalOfferPaymentParams {
723+
/// payer_offer: Some(payer_offer),
724+
/// ..Default::default()
725+
/// };
726+
/// ```
727+
///
728+
/// [`OffersMessageFlow::create_compact_offer_builder`]: crate::offers::flow::OffersMessageFlow::create_compact_offer_builder
729+
pub payer_offer: Option<Offer>,
701730
}
702731

703732
impl Default for OptionalOfferPaymentParams {
@@ -709,6 +738,8 @@ impl Default for OptionalOfferPaymentParams {
709738
retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)),
710739
#[cfg(not(feature = "std"))]
711740
retry_strategy: Retry::Attempts(3),
741+
contact_secrects: None,
742+
payer_offer: None,
712743
}
713744
}
714745
}
@@ -13324,6 +13355,8 @@ where
1332413355
payment_id,
1332513356
None,
1332613357
create_pending_payment_fn,
13358+
optional_params.contact_secrects,
13359+
optional_params.payer_offer,
1332713360
)
1332813361
}
1332913362

@@ -13353,6 +13386,8 @@ where
1335313386
payment_id,
1335413387
Some(offer.hrn),
1335513388
create_pending_payment_fn,
13389+
optional_params.contact_secrects,
13390+
optional_params.payer_offer,
1335613391
)
1335713392
}
1335813393

@@ -13395,6 +13430,8 @@ where
1339513430
payment_id,
1339613431
None,
1339713432
create_pending_payment_fn,
13433+
optional_params.contact_secrects,
13434+
optional_params.payer_offer,
1339813435
)
1339913436
}
1340013437

@@ -13403,6 +13440,7 @@ where
1340313440
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
1340413441
payer_note: Option<String>, payment_id: PaymentId,
1340513442
human_readable_name: Option<HumanReadableName>, create_pending_payment: CPP,
13443+
contacts: Option<ContactSecrets>, payer_offer: Option<Offer>,
1340613444
) -> Result<(), Bolt12SemanticError> {
1340713445
let entropy = &*self.entropy_source;
1340813446
let nonce = Nonce::from_entropy_source(entropy);
@@ -13428,6 +13466,20 @@ where
1342813466
Some(hrn) => builder.sourced_from_human_readable_name(hrn),
1342913467
};
1343013468

13469+
let builder = if let Some(secrets) = contacts.as_ref() {
13470+
builder.contact_secrets(secrets.clone())
13471+
} else {
13472+
builder
13473+
};
13474+
13475+
// Add payer offer only if provided by the user.
13476+
// If the user explicitly wants to include an offer, they should provide it via payer_offer parameter.
13477+
let builder = if let Some(offer) = payer_offer {
13478+
builder.payer_offer(&offer)
13479+
} else {
13480+
builder
13481+
};
13482+
1343113483
let invoice_request = builder.build_and_sign()?;
1343213484
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
1343313485

@@ -16076,7 +16128,7 @@ where
1607616128
self.pending_outbound_payments
1607716129
.received_offer(payment_id, Some(retryable_invoice_request))
1607816130
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)
16079-
});
16131+
}, None, None);
1608016132
if offer_pay_res.is_err() {
1608116133
// The offer we tried to pay is the canonical current offer for the name we
1608216134
// wanted to pay. If we can't pay it, there's no way to recover so fail the

lightning/src/ln/offers_tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
683683
quantity: None,
684684
payer_note_truncated: None,
685685
human_readable_name: None,
686+
contact_secret: None,
687+
payer_offer: None,
686688
},
687689
});
688690
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
@@ -841,6 +843,8 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
841843
quantity: None,
842844
payer_note_truncated: None,
843845
human_readable_name: None,
846+
contact_secret: None,
847+
payer_offer: None,
844848
},
845849
});
846850
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
@@ -962,6 +966,8 @@ fn pays_for_offer_without_blinded_paths() {
962966
quantity: None,
963967
payer_note_truncated: None,
964968
human_readable_name: None,
969+
contact_secret: None,
970+
payer_offer: None,
965971
},
966972
});
967973

@@ -1229,6 +1235,8 @@ fn creates_and_pays_for_offer_with_retry() {
12291235
quantity: None,
12301236
payer_note_truncated: None,
12311237
human_readable_name: None,
1238+
contact_secret: None,
1239+
payer_offer: None,
12321240
},
12331241
});
12341242
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
@@ -1294,6 +1302,8 @@ fn pays_bolt12_invoice_asynchronously() {
12941302
quantity: None,
12951303
payer_note_truncated: None,
12961304
human_readable_name: None,
1305+
contact_secret: None,
1306+
payer_offer: None,
12971307
},
12981308
});
12991309

@@ -1391,6 +1401,8 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
13911401
quantity: None,
13921402
payer_note_truncated: None,
13931403
human_readable_name: None,
1404+
contact_secret: None,
1405+
payer_offer: None,
13941406
},
13951407
});
13961408
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);

lightning/src/offers/flow.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,46 @@ where
569569
Ok((builder.into(), nonce))
570570
}
571571

572+
/// Creates a minimal [`OfferBuilder`] with derived metadata and an optional blinded path.
573+
///
574+
/// If `intro_node_id` is `None`, creates an offer with no blinded paths (~70 bytes) suitable
575+
/// for scenarios like BLIP-42 where the payer intentionally shares their contact info.
576+
///
577+
/// If `intro_node_id` is `Some`, creates an offer with a single blinded path (~200 bytes)
578+
/// providing privacy/routability for unannounced nodes. The intro node must be a public
579+
/// peer (routable via gossip) with an outbound channel.
580+
///
581+
/// # Privacy
582+
///
583+
/// - `None`: Exposes the derived signing pubkey directly without blinded path privacy
584+
/// - `Some`: Intro node learns payer identity (choose trusted/routable peer)
585+
///
586+
/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
587+
pub fn create_compact_offer_builder<ES: Deref>(
588+
&self, entropy_source: ES, intro_node_id: Option<PublicKey>,
589+
) -> Result<OfferBuilder<'_, DerivedMetadata, secp256k1::All>, Bolt12SemanticError>
590+
where
591+
ES::Target: EntropySource,
592+
{
593+
match intro_node_id {
594+
None => {
595+
// Use the internal builder but don't add any paths
596+
self.create_offer_builder_intern(
597+
&*entropy_source,
598+
|_, _, _| Ok(core::iter::empty()),
599+
)
600+
.map(|(builder, _)| builder)
601+
},
602+
Some(node_id) => {
603+
// Delegate to create_offer_builder with a single-peer list to reuse the router logic
604+
self.create_offer_builder(
605+
entropy_source,
606+
vec![MessageForwardNode { node_id, short_channel_id: None }],
607+
)
608+
},
609+
}
610+
}
611+
572612
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the
573613
/// [`OffersMessageFlow`], and any corresponding [`InvoiceRequest`] can be verified using
574614
/// [`Self::verify_invoice_request`]. The offer will expire at `absolute_expiry` if `Some`,

0 commit comments

Comments
 (0)