Skip to content
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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Mar 3, 2025

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 of ChannelManager, while allowing ChannelManager to be parameterized over Flow.

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.

@ldk-reviews-bot
Copy link

👋 Hi! I see this is a draft PR.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@shaavan
Copy link
Member Author

shaavan commented Mar 3, 2025

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 ChannelManager.

What's in the current version:

  • Added initial utilities for creating Offers, Refunds, Blinded Paths, and handling message enqueue/dequeue directly within Flow.
  • Started using these utilities inside Offer payers and builders.

About the CI failures:

  • The CI is failing because of some documentation issues in this version.
  • All functional tests for the std case are passing locally.

If this approach looks good, I'll expand it to cover Offer handlers next!

@shaavan
Copy link
Member Author

shaavan commented Mar 3, 2025

@TheBlueMatt, @jkczyz
A gentle ping! :)

Copy link
Contributor

@jkczyz jkczyz left a 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 BlindedPaymentPaths.

message_router: MR,

our_network_pubkey: PublicKey,
highest_seen_timestamp: AtomicUsize,
Copy link
Contributor

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.

Copy link
Member Author

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!

Comment on lines +44 to +52
#[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)>>,
Copy link
Contributor

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.

Copy link
Member Author

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:

  1. 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.
  2. 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!

Comment on lines +186 to +221
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)
}
Copy link
Contributor

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.

Copy link
Member Author

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.

MR::Target: MessageRouter,
{
fn create_offer_builder(
&self, nonce: Nonce,
Copy link
Contributor

@jkczyz jkczyz Mar 4, 2025

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 BlindedPaths? Otherwise, the onus is on the user to pass the correct context. So this implies the blinded paths should be created within this method.

Copy link
Member Author

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!

Comment on lines +252 to +279
fn create_invoice_builder<'a>(
&'a self, refund: &'a Refund, payment_paths: Vec<BlindedPaymentPath>,
payment_hash: PaymentHash,
) -> Result<InvoiceBuilder<'a, DerivedSigningPubkey>, Bolt12SemanticError> {
Copy link
Contributor

@jkczyz jkczyz Mar 4, 2025

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?

Copy link
Member Author

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!

}

fn create_blinded_paths(
&self, peers: Vec<MessageForwardNode>, context: MessageContext,
Copy link
Contributor

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.


#[cfg(feature = "dnssec")]
use crate::onion_message::dns_resolution::DNSResolverMessage;

pub trait Flow {
Copy link
Contributor

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).

Copy link
Member Author

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!

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.

Were we going to move most of the OffersMessageHandler logic into flow.rs?

@shaavan
Copy link
Member Author

shaavan commented Mar 6, 2025

Updated from pr3639.01 to pr3639.02 (diff):
Addressed @jkczyz comments

Changes:

  • Implement chain::Listen of OffersMessageFlow, to allow keep the highest_seen_timestamp synced.

@shaavan
Copy link
Member Author

shaavan commented Mar 6, 2025

@TheBlueMatt

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 Bolt12 payers/builders from ChannelManager. I wanted to first get a concept ACK on this approach before proceeding with abstracting the handlers out of ChannelManager.

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.

4 participants