Skip to content

Enable Creation of Offers and Refunds Without Blinded Path #3246

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Aug 16, 2024

PR Description:

An Offer and a Refund can exist without a blinded path, where the signing_pubkey is used to determine the destination. While we could already handle Offers and Refunds without a blinded path, we didn’t have a way to create them like this. This PR addresses that gap and simplifies the blinded path creation flow.

The Key Changes:

  1. Pass Router to Builders:
    Both create_offer_builder and create_refund_builder now take a router as a parameter. This gives users control over how blinded paths are created. For example, users can now choose not to include any blinded paths, or use a custom router for specific behavior.

  2. Unified Blinded Path Creation:
    The Blinded Path creation has been simplified by only keeping a single function for Blinded Path creation, i.e., create_blinded_path. Older, separate variants like create_compact_blinded_paths and create_blinded_paths_using_absolute_expiry are now redundant and have been removed. This makes the flow cleaner and easier to maintain.

  3. Router-Driven Flexibility:
    The DefaultMessageRouter now uses a RouterConfig to allow it to create any kind of blinded path (if any) to create. This simplifies the flow, while maintaining the same functionality.

This PR makes blinded path creation more straightforward and gives users better control, while preserving the core logic of the MessageRouter.

blocked on #3412

Copy link

codecov bot commented Aug 16, 2024

Codecov Report

Attention: Patch coverage is 96.88312% with 12 lines in your changes missing coverage. Please review.

Project coverage is 90.58%. Comparing base (3da69f7) to head (65401b9).
Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/onion_message/messenger.rs 85.71% 5 Missing ⚠️
lightning/src/offers/flow.rs 97.19% 0 Missing and 3 partials ⚠️
lightning/src/ln/channelmanager.rs 96.82% 0 Missing and 2 partials ⚠️
lightning-dns-resolver/src/lib.rs 66.66% 1 Missing ⚠️
lightning/src/ln/offers_tests.rs 99.07% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3246      +/-   ##
==========================================
+ Coverage   89.72%   90.58%   +0.86%     
==========================================
  Files         164      164              
  Lines      133333   142506    +9173     
  Branches   133333   142506    +9173     
==========================================
+ Hits       119627   129084    +9457     
+ Misses      11031    10828     -203     
+ Partials     2675     2594      -81     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shaavan
Copy link
Member Author

shaavan commented Aug 19, 2024

Updated from pr3246.01 to pr3246.02 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

@shaavan
Copy link
Member Author

shaavan commented Aug 19, 2024

Updated from pr3246.02 to pr3246.03 (diff):

Changes:

  1. Fix docs.
  2. Introduce tests for offer and refund with no blinded paths.

@shaavan shaavan marked this pull request as ready for review August 19, 2024 13:30
@shaavan
Copy link
Member Author

shaavan commented Aug 20, 2024

Updated from pr3246.03 to pr3246.04 (diff):

Changes:

  1. Rebase on main, to resolve merge conflicts.
  2. Fix CI.

assert_eq!(refund.payer_id(), alice_id);
assert!(refund.paths().is_empty());
}

/// Checks that blinded paths are compact for short-lived offers.
#[test]
fn creates_short_lived_offer() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming at least for the following tests is no longer relevant, it seems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, with the introduction of BlindedPathParameters, it seems like the behavior these tests were checking isn't relevant anymore. Should we think about removing these tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wait on the resolution of #3246 (comment). We'll ultimately want to test cases allowed by whatever interface is exposed to the user.

@shaavan
Copy link
Member Author

shaavan commented Sep 4, 2024

Updated from pr3246.04 to pr3246.05 (diff):

Changes:

  1. Rebase on main.

@shaavan
Copy link
Member Author

shaavan commented Sep 5, 2024

Updated from pr3246.05 to pr3246.06 (diff):
Addressed @jkczyz comments

Changes:

  1. Removed the global constant PATHS_PLACEHOLDER, and instead use a default constructor, and local DEFAULT_VALUE.
  2. Remove the redundant functions in their appropriate commits.
  3. Use match to avoid mut variables.

@shaavan
Copy link
Member Author

shaavan commented Sep 19, 2024

Updated from pr3246.06 to pr3246.07 (diff):

Changes:

  1. Introduce a new approach using the BlindedPathType enum.
  2. The enum allows for the specification of the type of Blinded Path (Compact or Full) that a user can specify to create the desired type of Blinded Path.
  3. Update offer_builder and refund_builder so that user can explicitly specify the type of Blinded Path they want to create.
  4. Update the Blinded Path creation flow so that only one function flow is responsible for creating both kinds of Blinded Paths.

@shaavan
Copy link
Member Author

shaavan commented Sep 19, 2024

Updated from pr3246.07 to pr3246.08 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

@shaavan shaavan changed the title Introduce BlindedPathParams Introduce BlindedPathType enum Sep 24, 2024
@shaavan
Copy link
Member Author

shaavan commented Oct 3, 2024

Updated from pr3246.08 to pr3246.09 (diff):

Changes:

  1. Rebase on main, and fix ci.

let context = MessageContext::Offers(context);
let path = $self
.create_blinded_paths(context)
.and_then(|paths| paths.into_iter().next().ok_or(()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... @TheBlueMatt I don't think this interface is sufficient to allow someone to specify they want more than one path in the offer, unless we include all the paths returned by the MessageRouter instead of just one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, IMO we should include all paths - if the router wants to do something funky in the offer, so be it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we have DefaultMessageRouter do?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we currently do (use the context of why we're asking for a path to decide how many paths to include)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose DefaultMessageRouter could use both MessageContex and BlindedPathType -- or which MessageRouter method is called, if we revert to that state -- to determine if more than one path should be returned. That way we'd allow users to pass BlindedPathType::Full to create_offer_builder such that they can create an offer with more than one path (e.g., when an offer doesn't need to be in a QR code).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that makes sense but then I'm confused about your previous comment about keeping get interface the same as it is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we change MessageRouter to only have one method, then we need to pass in BlindedPathType. But then the peers parameter will need to be Vec<MessageForwardNode> even when used with BlindedPathType::Full instead of Vec<PublicKey>. So callers could pass in a MessageForwardNode with an scid even though it isn't expected, resulting in the last hop being compact.

Copy link
Member Author

@shaavan shaavan Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So callers could pass in a MessageForwardNode with an scid even though it isn't expected, resulting in the last hop being compact.

Makes sense! I have reintroduced the two function flow back in pr3246.11

Also, I have updated the DefaultMessageRouter's create_compact_blinded_paths to only return a single path in pr3246.11

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why this is a concern, if the MessageRouter insists on a specific type of blinded path in general, so what? Ideally the MessageRouter gets to pick what it wants to do, though I think we're not allowed to have a compact introduction point in reply paths? Even in that case, though, ISTM we can communicate the restrictions and if the MessageRouter is buggy we can just fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why this is a concern, if the MessageRouter insists on a specific type of blinded path in general, so what? Ideally the MessageRouter gets to pick what it wants to do,

Nothing is preventing a MessageRouter from doing so. It would just need access to the channels.

It's just kinda ugly for DefaultMessageRouter to need to clear short_channel_id depending on how it is called. Plus, ChannelManager needs to do additional lookups to get this information just to have it discarded. I'm fine either way, but it doesn't optimize the usual case (i.e., use non-compact paths for reply paths).

though I think we're not allowed to have a compact introduction point in reply paths? Even in that case, though, ISTM we can communicate the restrictions and if the MessageRouter is buggy we can just fail.

Yeah, though it doesn't look like we are enforcing this at the moment.

@shaavan
Copy link
Member Author

shaavan commented Oct 11, 2024

Updated from pr3246.09 to pr3246.10 (diff):
Addressed @jkczyz comments

Changes:

  1. Update BlindedPathType documentation.
  2. Update code to amend all returned paths by MessageRouter to offers, and refund
  3. DRY create_offer_builder, and create_refund_builder

@TheBlueMatt
Copy link
Collaborator

I'm definitely okay with having methods to override the router used, and like the changes to not decide in ChannelManager if a path should be compact or not, but I'm a bit confused because now it seems like we default to doing the wrong thing - if an offer is short-lived we never build a compact blinded path unless the user manually asks for it by passing a CompactMessageRouter? That seems like an important regression (unless we think actually its not worth it to have compact BPs in short-lived offers cause most offers are long-lived?).

@jkczyz
Copy link
Contributor

jkczyz commented Apr 15, 2025

I'm definitely okay with having methods to override the router used, and like the changes to not decide in ChannelManager if a path should be compact or not, but I'm a bit confused because now it seems like we default to doing the wrong thing - if an offer is short-lived we never build a compact blinded path unless the user manually asks for it by passing a CompactMessageRouter? That seems like an important regression (unless we think actually its not worth it to have compact BPs in short-lived offers cause most offers are long-lived?).

It could create compact paths by default if ChannelManager were parameterized by CompactMessageRouter. And besides from having the user explicitly pass CompactMessageRouter to a one of the methods, how could it be done without having ChannelManager decide?

I guess by passing the Option<Duration> through from ChannelManager to MessageRouter? Not sure it if is even worth having CompactMessageRouter then. NullMessageRouter may be sufficient to pass as an override. Don't have strong opinion, though it would be nice if create_offer_builder didn't need an optional parameter.

@shaavan shaavan force-pushed the blinded_api branch 2 times, most recently from 7b9b2ea to 49c6c3a Compare April 18, 2025 18:32
@shaavan
Copy link
Member Author

shaavan commented Apr 18, 2025

Updated from pr3246.18 to pr3246.19 (diff):
Addressed @jkczyz comments

Changes:

  • Expanded NullMessageRouter, and CompactMessageRouter document.

@TheBlueMatt
Copy link
Collaborator

It could create compact paths by default if ChannelManager were parameterized by CompactMessageRouter. And besides from having the user explicitly pass CompactMessageRouter to a one of the methods, how could it be done without having ChannelManager decide?

I guess by passing the Option through from ChannelManager to MessageRouter?

Right, this is what I was thinking - give the router enough information to decide and let it decide, then let users override the router if they want to override the decision.

Not sure it if is even worth having CompactMessageRouter then. NullMessageRouter may be sufficient to pass as an override.

I guess if you want to always use a compact path (even for long-lived paths)? Could also have some kind of configurable router that just lets you pick the time limit.

Don't have strong opinion, though it would be nice if create_offer_builder didn't need an optional parameter.

Agreed, it can be a separate function tho.

@shaavan
Copy link
Member Author

shaavan commented May 5, 2025

@TheBlueMatt

I see the point you're suggesting, Matt, and I agree there’s some convenience in having the router automatically decide the blinded path type based on Option<Duration>. But I think having the user explicitly call the offer builder with a CompactMessageRouter is just as convenient—and arguably cleaner.

The current approach has the benefit of being explicit and predictable. By default, we always generate a full-length path, providing a consistent baseline across all offers. If a user wants a compact path—for example, for short-lived offers—they can opt into it clearly via the router they pass in. Conversely, if the user parameterizes ChannelManager with a CompactMessageRouter, they’ll consistently get compact paths—unless they manually override that choice.

This trades away automatic decision-making by the router in favor of something I believe is more valuable in the long run: user awareness and control. It also avoids introducing heuristics that could surprise users unless they’re quite familiar with the internal logic.

In that sense, prioritizing clear defaults and explicit opt-in behavior, I believe, makes the design more intuitive and easier to reason about over time.

Curious to hear your thoughts—thanks!

@jkczyz
Copy link
Contributor

jkczyz commented May 6, 2025

I see the point you're suggesting, Matt, and I agree there’s some convenience in having the router automatically decide the blinded path type based on Option<Duration>. But I think having the user explicitly call the offer builder with a CompactMessageRouter is just as convenient—and arguably cleaner.

Regarding this point, I tend to agree that removing the Option<Duration> from methods like create_offer_builder is more desirable. The existing behavior uses this to set the expiry and provide additional information for creating the path. But upon returning an OfferBuilder, the user can then override the expiry with a longer one and be left with a long-lived offer using compact blinded paths.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grr, not sure why I responded to this, sorry.

This trades away automatic decision-making by the router in favor of something I believe is more valuable in the long run: user awareness and control.

I think this is the fundamental disagreement, indeed. While we should always put the user in control, allowing them to do what they want to do, I think the current approach fails the "user awareness" goal. In general, IMO we should assume that users will not read every line of our documentation (there's a ton of it!) but rather will try to call methods and if it works they'll leave it.

In this specific case, I think that the most common use-case for BOLT12 will prefer a more compact QR code/offer as they're receiving a single payment (as most "end users" of lightning will have single LSPs where they have a single channel which has the same SCID alias for its lifetime, splicing in and out as needed but with the SCID alias constant). If the SCID ever does become invalid, a pubkey-based blinded path would become similarly invalid as they are no longer using the same LSP.

Thus, assuming most users will just "do the default thing that works", they're actually going to get something they don't want.

Now, I'm not defending picking it based on the expiry time, in the above scenario, users probably want a compact offer even if it has an extended expiry time, so maybe the right answer is to just keep what we have here and change the default to always use a compact offer.

@shaavan
Copy link
Member Author

shaavan commented Jun 19, 2025

Updated from pr3246.19 to pr3246.20 (diff):
Addressed @TheBlueMatt comments

Changes:

  1. Update DefaultMessageRouter to create Compact BlindedMessagePath by default.
  2. When a user wants to create full-length blinded path, they can parameterise using the NodeIdMessageRouter

shaavan added 2 commits June 19, 2025 23:50
Previously, the `compact_paths` flag was only used to determine whether to
use a compact introduction node when creating compact blinded paths.

With the upcoming change to accept `MessageForwardNode` in
`create_blinded_paths`, there's a risk of SCIDs being passed (and used)
even when the user intends to create a full-length blinded path.

This patch updates the logic in `create_blinded_paths_from_iter` to
ignore SCIDs unless `compact_paths` is explicitly true—preserving correct
behavior for full-length blinded paths.
To prepare for supporting both standard and compact blinded paths,
this commit updates the `create_blinded_paths` function to take a
`Vec<MessageForwardNode>` as input.

This change ensures the function has all the information it needs
to handle both types of blinded path creation. This refactor that
sets the stage for upcoming enhancements.
shaavan added a commit to shaavan/rust-lightning that referenced this pull request Jun 23, 2025
Reasoning:

This change aligns `DefaultMessageRouter`'s default behavior with the most common
and practical usage: generating compact blinded paths for BOLT12 offers.

While configurability is important, most users won't read all documentation and
will rely on defaults that "just work." In line with this PR's principle—
"One `MessageRouter`, one type of `BlindedPath`"—the previous default would
silently produce full blinded paths, even when a compact one would have been
more appropriate.

In typical setups (e.g., a user connected to a single LSP), the SCID alias
remains stable for the channel's lifetime. Compact paths are not only sufficient
in this case, but also result in smaller, more efficient offers. And if the
alias becomes invalid, a pubkey-based path wouldn't help either—so compact
remains the better default.

In brief: This commit makes the default behavior match what users actually want.

Thanks to [@TheBlueMatt](https://github.com/TheBlueMatt) for the original reasoning.
**Discussion link:** [lightningdevkit#3246 (pull request review)](lightningdevkit#3246 (review))
shaavan added a commit to shaavan/rust-lightning that referenced this pull request Jun 23, 2025
Reasoning:

This change aligns `DefaultMessageRouter`'s default behavior with the most common
and practical usage: generating compact blinded paths for BOLT12 offers.

While configurability is important, most users won't read all documentation and
will rely on defaults that "just work." In line with this PR's principle—
"One `MessageRouter`, one type of `BlindedPath`"—the previous default would
silently produce full blinded paths, even when a compact one would have been
more appropriate.

In typical setups (e.g., a user connected to a single LSP), the SCID alias
remains stable for the channel's lifetime. Compact paths are not only sufficient
in this case, but also result in smaller, more efficient offers. And if the
alias becomes invalid, a pubkey-based path wouldn't help either—so compact
remains the better default.

In brief: This commit makes the default behavior match what users actually want.

Thanks to [@TheBlueMatt](https://github.com/TheBlueMatt) for the original reasoning.
**Discussion link:** [lightningdevkit#3246 (pull request review)](lightningdevkit#3246 (review))
shaavan added a commit to shaavan/rust-lightning that referenced this pull request Jun 24, 2025
Reasoning:

This change aligns `DefaultMessageRouter`'s default behavior with the most common
and practical usage: generating compact blinded paths for BOLT12 offers.

While configurability is important, most users won't read all documentation and
will rely on defaults that "just work." In line with this PR's principle—
"One `MessageRouter`, one type of `BlindedPath`"—the previous default would
silently produce full blinded paths, even when a compact one would have been
more appropriate.

In typical setups (e.g., a user connected to a single LSP), the SCID alias
remains stable for the channel's lifetime. Compact paths are not only sufficient
in this case, but also result in smaller, more efficient offers. And if the
alias becomes invalid, a pubkey-based path wouldn't help either—so compact
remains the better default.

In brief: This commit makes the default behavior match what users actually want.

Thanks to [@TheBlueMatt](https://github.com/TheBlueMatt) for the original reasoning.
**Discussion link:** [lightningdevkit#3246 (pull request review)](lightningdevkit#3246 (review))
shaavan added 7 commits June 24, 2025 17:25
To make the purpose of each `MessageRouter` implementation unambiguous,
this commit sets a direction where the type of `MessageRouter` used
deterministically defines the kind of blinded paths created.

As a step toward this goal, two new default routers are introduced:
- `NodeIdMessageRouter` – creates full-length blinded paths using the
  peer's node ID.
- `NullMessageRouter` – intentionally creates no blinded paths.
To allow choosing different message router types for testing nodes,
convert `TestMessageRouter` to an enum with variants `DefaultMessageRouter`
and `NodeIdMessageRouter`. This provides better flexibility when testing
various scenarios.
Reasoning:

This change aligns `DefaultMessageRouter`'s default behavior with the most common
and practical usage: generating compact blinded paths for BOLT12 offers.

While configurability is important, most users won't read all documentation and
will rely on defaults that "just work." In line with this PR's principle—
"One `MessageRouter`, one type of `BlindedPath`"—the previous default would
silently produce full blinded paths, even when a compact one would have been
more appropriate.

In typical setups (e.g., a user connected to a single LSP), the SCID alias
remains stable for the channel's lifetime. Compact paths are not only sufficient
in this case, but also result in smaller, more efficient offers. And if the
alias becomes invalid, a pubkey-based path wouldn't help either—so compact
remains the better default.

In brief: This commit makes the default behavior match what users actually want.

Thanks to [@TheBlueMatt](https://github.com/TheBlueMatt) for the original reasoning.
**Discussion link:** [lightningdevkit#3246 (pull request review)](lightningdevkit#3246 (review))
To simplify blinded path creation and uphold the principle of "One
`MessageRouter`, one `BlindedPath` type," this commit updates
`create_offer_builder` to use the `create_blinded_paths` method of the
`MessageRouter`.

Now, when `create_offer_builder` is called, the offer will be created using
the `MessageRouter` implementation that the `ChannelManager` or
`OffersMessageFlow` is parameterized with.

If a user wishes to create an offer with a different type of blinded path,
they can explicitly use `create_offer_builder_using_router`, which allows
passing a custom `MessageRouter`.

The reasoning behind this change is to give users clearer, more deterministic
control over the type of blinded path used in the offer. It also improves
user awareness, ensuring that creating a non-default blinded path becomes an
*intentional choice*.
This change mirrors the previous update to `create_offer_builder`, applying the
**“One `MessageRouter`, one `BlindedPath` type”** principle to refund creation.

Now, `create_refund_builder` uses the `create_blinded_paths` method of the
`MessageRouter` associated with the `ChannelManager` or `OffersMessageFlow`.

For non-default path behavior, users can call `create_refund_builder_using_router`
and pass a custom `MessageRouter`.

See previous commit for detailed reasoning.
This commit completes the series implementing the principle:
**“One `MessageRouter`, one `BlindedPath` type.”**

As the final step, it removes now-redundant variations of the
blinded path creation functions, streamlining the API and
simplifying the blinded path creation process.
Introduced tests to validate the behavior of Offers and Refunds
created without blinded paths, using `NullMessageRouter`.
@shaavan
Copy link
Member Author

shaavan commented Jun 24, 2025

Updated from pr3246.20 to pr3246.21 (diff):

Changes:

  1. Rebase on main
  2. Clean-up commits history and messages.
  3. Address other minor nit-changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants