-
Notifications
You must be signed in to change notification settings - Fork 390
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
Introduce Flow utilities and OffersMessageFlow implementation #3639
base: main
Are you sure you want to change the base?
Conversation
👋 I see @TheBlueMatt was un-assigned. |
Right now, this PR is a proof of concept for the approach—it introduces a basic set of utilities to move a significant chunk of Offers-related code out of What's in the current version:
About the CI failures:
If this approach looks good, I'll expand it to cover Offer handlers next! |
@TheBlueMatt, @jkczyz |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's iterate a bit more on this. We should have the MessageContext
creation and thus the BlindedPath
creation be the domain of OffersMessageFlow
, IMO. This means passing a closure to the utility functions to create the blinded paths. Likewise with BlindedPaymentPath
s.
message_router: MR, | ||
|
||
our_network_pubkey: PublicKey, | ||
highest_seen_timestamp: AtomicUsize, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd need to update this any time a block is processed. So either we need an Arc
to share with ChannelManager
or have Flow
require chain::Listen
such that ChannelManager
(or whoever owns the Flow
) can call block_connected
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense! I’ve implemented the chain::Listen
trait for OffersMessageFlow
in pr3639.02. This allows block_connected
to be called as needed.
Let me know if this approach aligns with what you had in mind! Thanks a lot for the pointer!
#[cfg(not(any(test, feature = "_test_utils")))] | ||
pending_offers_messages: Mutex<Vec<(OffersMessage, MessageSendInstructions)>>, | ||
#[cfg(any(test, feature = "_test_utils"))] | ||
pub(crate) pending_offers_messages: Mutex<Vec<(OffersMessage, MessageSendInstructions)>>, | ||
|
||
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>, | ||
|
||
#[cfg(feature = "dnssec")] | ||
pending_dns_onion_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the idea here was to have the owner such as ChannelManager
own the message queues since it would implement the message handlers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was deciding between two designs: having ChannelManager
hold the queues or having OffersMessageFlow
hold them. I chose the latter because:
- The long-term goal is to fully separate offers-related code from
ChannelManager
, and moving the queues out will be a major step in that direction. - Keeping the queues within
OffersMessageFlow
helps maintain cleaner utility function signatures, as users don’t need to explicitly pass the queues as parameters.
Let me know your thoughts on this approach!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like having the queues in the flow? It makes it more of a useful "object", rather than a "bag of functions".
lightning/src/offers/flow.rs
Outdated
fn create_offer_builder( | ||
&self, nonce: Nonce, | ||
) -> Result<OfferBuilder<DerivedMetadata, secp256k1::All>, Bolt12SemanticError> { | ||
let node_id = self.get_our_node_id(); | ||
let expanded_key = &self.inbound_payment_key; | ||
let secp_ctx = &self.secp_ctx; | ||
|
||
let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) | ||
.chain_hash(self.chain_hash); | ||
|
||
Ok(builder) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd question what value utilities like these are really providing to the the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea I was exploring was to create a mid-level API that remains modular enough to be used in multiple contexts.
For example, self.flow.create_offer_builder
simplifies both create_offer_builder
and create_async_receive_offer_builder
, making the API more reusable and reducing duplication.
Would love to hear your thoughts on whether this with indented design approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm similarly skeptical of adding methods that do "nothing". In cases where we're delegating blinded message path building to the flow, fine, but here I'm pretty skeptical this is adding much value.
lightning/src/offers/flow.rs
Outdated
MR::Target: MessageRouter, | ||
{ | ||
fn create_offer_builder( | ||
&self, nonce: Nonce, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the Nonce
could be generated from the EntropySource
. Though I imagine we need the Nonce
for the MessageContext
. Don't we want OffersMessageFlow
to determine the correct MessageContext
to give to the BlindedPath
s? Otherwise, the onus is on the user to pass the correct context. So this implies the blinded paths should be created within this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense! My initial reasoning was to keep offer building and blinded path creation separate, as this allowed us to use the same self.flow.create_offer_builder
for both create_offer_builder
(where we create & append the path later) and create_async_receive_offer_builder
(where we already have a vector of paths to append).
That said, I think we can take a middle ground by passing an optional closure as input—if present, it would handle the creation and appending of paths. This keeps things flexible while reducing the onus on the user to manually pass the correct context.
Let me know what you think of this approach. Thanks!
lightning/src/offers/flow.rs
Outdated
fn create_invoice_builder<'a>( | ||
&'a self, refund: &'a Refund, payment_paths: Vec<BlindedPaymentPath>, | ||
payment_hash: PaymentHash, | ||
) -> Result<InvoiceBuilder<'a, DerivedSigningPubkey>, Bolt12SemanticError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note we need this not only for refunds but also for handling invoice requests. I'm wondering though if we'd rather want a method that does more similar to request_refund_payment
and pay_for_offer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking through the section of code where we create an invoice from an invoice request, it seems significantly different from invoice creation from a refund. Given our current trait design, introducing a separate function feels like a cleaner solution.
On the other hand, we could design the Flow
trait to be more similar to request_refund_payment
and pay_for_offer
, but wouldn’t that make it too high-level of an API?
@TheBlueMatt, would love to hear your thoughts on this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, I feel like it'd be nice to have a higher-level API on the flow. If the goal is to move bolt12-specific logic out of ChannelManager
as much as possible, we should target a high-level API and then make the Flow
object configurable where we see demand for configuration, IMO.
lightning/src/offers/flow.rs
Outdated
} | ||
|
||
fn create_blinded_paths( | ||
&self, peers: Vec<MessageForwardNode>, context: MessageContext, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, it seems OffersMessageFlow
should be the one determining which context to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, hiding the context in the flow gives it some material value for the user cause that's a bit of complexity that the user wouldn't have to implement.
lightning/src/offers/flow.rs
Outdated
|
||
#[cfg(feature = "dnssec")] | ||
use crate::onion_message::dns_resolution::DNSResolverMessage; | ||
|
||
pub trait Flow { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After going through the PR, I'm not sure how useful the Flow
trait is. Either the caller of the utilities can do any processing between calls or we can parameterize OffersMessageFlow
with traits (e.g., currency conversion).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I agree with your point. We could make OffersMessageFlow
a direct parameter of ChannelManager
without needing the Flow
trait as an intermediary. This way, we still remove Bolt12
responsibilities from ChannelManager
while giving users the flexibility to implement their own handlers using OffersMessageFlow
utilities.
@TheBlueMatt, I’d love to hear your thoughts on this approach!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it depends a bit on where we land on the API itself - if its high-level and we allow the OffersMessageFlow
to be configured where there is a need for options, great, we can drop the trait. If we end up with something where we want it to not be super configurable but rather overridable, we can keep the flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were we going to move most of the OffersMessageHandler
logic into flow.rs
?
Yes, that’s the goal! For this version, I’ve primarily abstracted out the |
Yea, its just a bit hard to evaluate without that change - there's presumably some state that can move into the flow handler... |
Updated from pr3639.02 to pr3639.03 (diff): Changes:
Points of notice:
|
Also, a gentle ping @TheBlueMatt and @jkczyz — thank you so much for all your inputs and ideas! Really grateful for them. |
self.flow.enqueue_invoice_request(invoice_request.clone(), payment_id, Some(nonce), |context| { | ||
self.create_blinded_paths(context) | ||
})?; | ||
|
||
Ok(()) | ||
create_pending_payment(&invoice_request, nonce) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheBlueMatt Here there a is change such that we enqueue the invoice requests prior to creating the pending payment. Seems like this could be an issue?
@shaavan Could you remind me why the ordering needed to be changed here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure!
I noticed that with the original ordering (create_pending_payment -> self.flow.enqueue_invoice_request
), if we try to pay for an offer and fail during the Blinded Path creation stage, we still end up creating a pending payment with that PaymentId
in pending_outbound_payments
.
So, if we try to call pay_for_offer
again with the same payment_id
, we hit a DuplicatePaymentId
error — because the payment was already enqueued in pending_outbound_payments
during the first (failed) attempt.
Happy to share a branch that reproduces the error if it's helpful. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheBlueMatt Here there a is change such that we enqueue the invoice requests prior to creating the pending payment. Seems like this could be an issue?
Hmm, don't see why it would be? Its all in a single PersistenceNotifierGuard
so the intermediate state cannot be written to disk (and we don't write the queue to disk anyway), and while in a theoretical edge case we could get the message out and get a response back faster than we can add the pending payment (causing us to consider the responding Invoice
un-requested), I'm somewhat skeptical its worth worrying about here.
debug_assert!(false); | ||
return Err(Bolt12SemanticError::MissingIssuerSigningPubkey); | ||
} | ||
self.flow.enqueue_invoice_request(invoice_request.clone(), payment_id, Some(nonce), |context| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunate that the user needs to know that they must pass the same nonce here as they had passed to create_invoice_request_builder
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true! Maybe we can add a short note in the enqueue_invoice_request
documentation mentioning that the nonce the user passes here must exactly match the nonce used to create the invoice_request
.
Alternatively, we could consider renaming the parameter to invoice_request_nonce
to make the connection clearer.
Let me know what you think — Thanks!
Err(()) => { | ||
self.abandon_payment_with_reason(payment_id, PaymentFailureReason::BlindedPathCreationFailed); | ||
if self.flow.enqueue_async_payment_messages(invoice, payment_id, |context| { | ||
self.create_blinded_paths(context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than passing a lambda for create_blinded_paths
to the flow, can we just pass the output of self.list_usable_channels()
and let the flow handle the rest? Seems like that would further reduce bolt12-specific code in ChannelManager
. Somewhat awkwardly, LNDK probably needs to be able to specify its own blinded payment paths, but would probably prefer the flow specify the blinded message paths. If we end up making this change that probably means we should add a separate method for LNDK to provide its own blinded path.
#[cfg(not(any(test, feature = "_test_utils")))] | ||
pending_offers_messages: Mutex<Vec<(OffersMessage, MessageSendInstructions)>>, | ||
#[cfg(any(test, feature = "_test_utils"))] | ||
pub(crate) pending_offers_messages: Mutex<Vec<(OffersMessage, MessageSendInstructions)>>, | ||
|
||
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>, | ||
|
||
#[cfg(feature = "dnssec")] | ||
pending_dns_onion_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like having the queues in the flow? It makes it more of a useful "object", rather than a "bag of functions".
lightning/src/offers/flow.rs
Outdated
fn create_offer_builder( | ||
&self, nonce: Nonce, | ||
) -> Result<OfferBuilder<DerivedMetadata, secp256k1::All>, Bolt12SemanticError> { | ||
let node_id = self.get_our_node_id(); | ||
let expanded_key = &self.inbound_payment_key; | ||
let secp_ctx = &self.secp_ctx; | ||
|
||
let builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) | ||
.chain_hash(self.chain_hash); | ||
|
||
Ok(builder) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm similarly skeptical of adding methods that do "nothing". In cases where we're delegating blinded message path building to the flow, fine, but here I'm pretty skeptical this is adding much value.
lightning/src/offers/flow.rs
Outdated
fn create_invoice_builder<'a>( | ||
&'a self, refund: &'a Refund, payment_paths: Vec<BlindedPaymentPath>, | ||
payment_hash: PaymentHash, | ||
) -> Result<InvoiceBuilder<'a, DerivedSigningPubkey>, Bolt12SemanticError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, I feel like it'd be nice to have a higher-level API on the flow. If the goal is to move bolt12-specific logic out of ChannelManager
as much as possible, we should target a high-level API and then make the Flow
object configurable where we see demand for configuration, IMO.
lightning/src/offers/flow.rs
Outdated
|
||
#[cfg(feature = "dnssec")] | ||
use crate::onion_message::dns_resolution::DNSResolverMessage; | ||
|
||
pub trait Flow { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it depends a bit on where we land on the API itself - if its high-level and we allow the OffersMessageFlow
to be configured where there is a need for options, great, we can drop the trait. If we end up with something where we want it to not be super configurable but rather overridable, we can keep the flow.
self.flow.enqueue_invoice_request(invoice_request.clone(), payment_id, Some(nonce), |context| { | ||
self.create_blinded_paths(context) | ||
})?; | ||
|
||
Ok(()) | ||
create_pending_payment(&invoice_request, nonce) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TheBlueMatt Here there a is change such that we enqueue the invoice requests prior to creating the pending payment. Seems like this could be an issue?
Hmm, don't see why it would be? Its all in a single PersistenceNotifierGuard
so the intermediate state cannot be written to disk (and we don't write the queue to disk anyway), and while in a theoretical edge case we could get the message out and get a response back faster than we can add the pending payment (causing us to consider the responding Invoice
un-requested), I'm somewhat skeptical its worth worrying about here.
This PR presents an alternative approach to #3412.
It introduces the
Flow
trait and its implementation,OffersMessageFlow
, as a mid-level API that abstracts most Offers-related logic out ofChannelManager
, while allowingChannelManager
to be parameterized overFlow
.This improves modularity and separation of concerns, making the Offers-related code more maintainable. Additionally, it provides a set of functional utilities for users who do not use
ChannelManager
in their workflow.