Skip to content

fix: bound outbound network latency in message handling - #843

Closed
grunch wants to merge 1 commit into
mainfrom
fix/bound-invoice-validation-latency
Closed

fix: bound outbound network latency in message handling#843
grunch wants to merge 1 commit into
mainfrom
fix/bound-invoice-validation-latency

Conversation

@grunch

@grunch grunch commented Jul 28, 2026

Copy link
Copy Markdown
Member

What

Bounds how long a single message handler can wait on an outbound network call, and stops doing one of those calls where it isn't needed yet.

Message handlers run sequentially on one event-loop task (app::run), so any request a handler makes delays every other message behind it for the full duration. A few of those requests had no effective upper bound, which left overall responsiveness dependent on third-party hosts the node doesn't control — a slow or unreachable LNURL endpoint, or an LND that isn't answering, would hold up unrelated traffic.

Changes

1. Cheap checks first in order_action

Fiat currency, fiat amount, sats amount, range limits and the quote lookup are all local. They now run before the payment request is looked at, so a malformed order is rejected without any further work.

2. Order creation validates the payment request without network I/O

validate_invoice takes a new InvoiceCheck argument:

  • Offline — syntax plus the full local BOLT11 rules, no HTTP.
  • Online — additionally resolves lightning addresses and LNURLs, as before.

Order creation uses Offline. It only records the payment request (the validation result was already discarded), and a lightning address still has to resolve when the payout is actually made — which is where an unreachable host is worth acting on. take_sell and add_invoice keep using Online: there the buyer is committing to a payout destination, so reachability is genuinely part of the decision. No change in behavior for those two paths.

3. Real bounds on the remaining calls

  • LNURL: a total wall-clock budget (LNURL_TOTAL_BUDGET, 5s) covering every round-trip in one operation, a 2s connect timeout, per-request timeout 10s → 4s, and a finite redirect chain (3). The per-request timeout alone wasn't sufficient, since resolv_ln_address makes two sequential requests where the second host comes from the first response.
  • LndConnector::new: a 10s connect timeout, where previously there was none. The RPCs after it are deliberately left unbounded — a timeout on settle_invoice would report failure for an operation LND may well have completed, which is a worse failure mode than waiting.

Test plan

  • cargo test — 1051 passed, 0 failed
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo fmt --all

New coverage:

  • lnurl_validation_against_unresponsive_host_is_time_bounded — a host that completes the TCP handshake and then never answers cannot hold validation open past the budget. Uses an ephemeral port via LNURL rather than the fixed 8080 that start_test_server owns, so it doesn't race the existing tests.
  • offline_validation_of_lnurl_does_no_network_io — the offline path returns immediately against that same unresponsive host.
  • offline_validation_still_rejects_malformed_payment_request / ..._still_enforces_bolt11_amount_checks — dropping the round-trip doesn't drop any local check.
  • validate_invoice_paths now runs its whole table under both modes, pinning down that they agree on every BOLT11 case.

Manual check: create an order with a lightning address on a host that is slow to respond, and confirm the order is created without the daemon pausing; then take a sell and confirm an unreachable address is still rejected at that point.

Follow-ups (not in this PR)

  • Per-pubkey throttling in SpamGate — it currently has no counter or token bucket, and the existing gate only applies to the nip44 transport while gift-wrap is still the default.
  • show_hold_invoice opens a fresh SqlitePool per take (util.rs) instead of reusing ctx.pool().
  • Moving slow work off the event loop properly. Worth noting that naive per-message tokio::spawn is not the answer: the current serialization is what keeps the order state machine free of races, so that needs per-order locking designed alongside it.

Summary by CodeRabbit

  • Bug Fixes
    • Improved invoice validation to reject unreachable Lightning payout destinations earlier during online payment flows.
    • Order creation now performs local checks before network-dependent validation, reducing unnecessary delays.
    • Added offline validation for Lightning Addresses and LNURLs when network access is not required.
    • Added safeguards so LNURL lookups and Lightning node connections time out instead of hanging indefinitely.
    • Preserved payment amount, fee, expiration, and format checks for Lightning invoices.

Message handlers run sequentially on a single event-loop task, so any
outbound request they make delays every other user's messages for as
long as it takes. Several of those requests had no effective upper
bound, which made overall responsiveness depend on hosts the node
doesn't control.

Three changes, smallest blast radius first:

1. `order_action` now runs its local checks (fiat currency, amounts,
   range limits, quote) before touching the payment request, so a
   malformed order costs nothing.

2. Order creation validates the payment request without network I/O.
   It only records the request — the validation result was already
   discarded — and a lightning address still has to resolve at payout
   time, which is where an unreachable host is actually actionable.
   Takes and invoice submission keep resolving over the network, where
   reachability is part of the decision being made.

3. LNURL operations get a total wall-clock budget covering every
   round-trip, a connect timeout, and a finite redirect chain. The
   per-request timeout alone was not enough: `resolv_ln_address` makes
   two sequential requests where the second host comes from the first
   response. `LndConnector::new` gets a connect timeout too; the RPCs
   after it are deliberately left alone, since a timeout on
   `settle_invoice` would report failure for work LND may have done.

Adds regression coverage that an unresponsive host cannot hold
validation open past the budget, that offline validation does no
network I/O, and that both check modes agree on every BOLT11 case.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Invoice validation now has explicit offline and online modes. Order creation uses offline checks after local validation, while payout-related actions use online checks. LNURL requests and LND connection setup now have bounded timeouts with expanded tests.

Changes

Invoice validation flow

Layer / File(s) Summary
Validation modes and local checks
src/util.rs, src/lightning/invoice.rs
InvoiceCheck::Offline and InvoiceCheck::Online select local-only or network-backed invoice validation, with tests covering both modes and BOLT11 rules.
Network operation time bounds
src/lnurl.rs, src/lightning/mod.rs, src/lightning/invoice.rs
LNURL requests and LND connection establishment receive explicit deadlines, with timeout behavior tested against unresponsive endpoints.
Application validation checkpoints
src/app/order.rs, src/app/add_invoice.rs, src/app/take_sell.rs
Order creation defers offline validation until local checks finish, while invoice submission and sell-taking use online validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OrderCreation
  participant PayoutAction
  participant validate_invoice
  participant LNURL
  OrderCreation->>validate_invoice: Offline validation after local checks
  PayoutAction->>validate_invoice: Online validation
  validate_invoice->>LNURL: Resolve Lightning Address or LNURL
  LNURL-->>validate_invoice: Validation result or bounded timeout
Loading

Possibly related PRs

  • MostroP2P/mostro#803: Updates invoice validation path tests related to the new offline and online modes.

Suggested reviewers: arkanoider

Poem

A rabbit checks each invoice bright,
Offline by day, online by night.
LNURL waits no endless hour,
LND now bows to timeout power.
BOLT11 hops clean and right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: bounding outbound network latency during message handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/bound-invoice-validation-latency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7232f16e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/app/order.rs
Comment on lines +142 to +143
let _invoice =
validate_invoice(&msg, &Order::from(order.clone()), InvoiceCheck::Offline).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep buy-order payout addresses reachable before publishing

When a buy-order maker supplies a syntactically valid Lightning address whose endpoint is unreachable, Offline now publishes the order although the previous validation rejected it. After a seller completes the trade, release_action settles the seller's hold invoice before calling do_payment; address resolution can then return early with an error that is ignored, without invoking check_failure_retries or requesting a replacement invoice. This leaves the order in SettledHoldInvoice with the buyer unpaid and no scheduled retry, so retain online validation for buy-order maker invoices or route resolution failures through the payment-failure recovery flow.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/util.rs (1)

1451-1481: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a /// doc comment now that the signature changed.

validate_invoice gained a check: InvoiceCheck parameter that materially changes behavior (network vs. no network), but the function itself still has no doc comment — only inline // comments. A short /// summary pointing callers at InvoiceCheck's variants would help prevent a future caller from picking the wrong mode.

As per coding guidelines, src/**/*.rs: "Document non-obvious public APIs with /// documentation comments."

📝 Suggested doc comment
+/// Validates the message's payment request, if present, against `order`.
+///
+/// See [`InvoiceCheck`] for what `Offline` vs `Online` mode changes.
 pub async fn validate_invoice(
     msg: &Message,
     order: &Order,
     check: InvoiceCheck,
 ) -> Result<Option<String>, MostroError> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util.rs` around lines 1451 - 1481, Add a concise /// documentation
comment immediately above validate_invoice describing its validation behavior
and directing callers to InvoiceCheck variants for choosing offline versus
online validation. Keep the existing implementation and inline comments
unchanged.

Source: Coding guidelines

src/lnurl.rs (1)

196-219: 🩺 Stability & Availability | 🔵 Trivial

Total budget (5s) is intentionally tighter than two sequential per-request caps (4s each).

resolv_ln_address_inner issues two sequential requests, each individually allowed up to LNURL_REQUEST_TIMEOUT (4s) by HTTP_CLIENT, but the outer LNURL_TOTAL_BUDGET caps the whole operation at 5s. This is explicitly documented as intentional, but it means a legitimate provider whose two hops together take, say, 3s + 3s will be reported as unreachable even though it would eventually have answered. Worth watching in production — if false "unreachable" rejections on take_sell/add_invoice (Online mode) turn out to be common for real LNURL providers, the budget may need widening.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lnurl.rs` around lines 196 - 219, The LNURL total timeout intentionally
caps both sequential requests at 5 seconds despite each request allowing 4
seconds; preserve this behavior and the existing timeout handling in
resolv_ln_address and resolv_ln_address_inner. No code change is required unless
production monitoring later shows frequent false unreachable errors, in which
case revisit LNURL_TOTAL_BUDGET.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lnurl.rs`:
- Around line 196-219: The LNURL total timeout intentionally caps both
sequential requests at 5 seconds despite each request allowing 4 seconds;
preserve this behavior and the existing timeout handling in resolv_ln_address
and resolv_ln_address_inner. No code change is required unless production
monitoring later shows frequent false unreachable errors, in which case revisit
LNURL_TOTAL_BUDGET.

In `@src/util.rs`:
- Around line 1451-1481: Add a concise /// documentation comment immediately
above validate_invoice describing its validation behavior and directing callers
to InvoiceCheck variants for choosing offline versus online validation. Keep the
existing implementation and inline comments unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c5a04609-09cb-42dd-b86f-b626ec889a54

📥 Commits

Reviewing files that changed from the base of the PR and between ec4a046 and d7232f1.

📒 Files selected for processing (7)
  • src/app/add_invoice.rs
  • src/app/order.rs
  • src/app/take_sell.rs
  • src/lightning/invoice.rs
  • src/lightning/mod.rs
  • src/lnurl.rs
  • src/util.rs

@grunch

grunch commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Superseded by #845, which reworks this branch after review.

The change to skip network validation at order creation rested on a wrong assumption: the payment request supplied there is not a throwaway value — it is persisted as orders.buyer_invoice and is what the payout is later sent to. Skipping the check there is only sound once a resolution failure at payout time is itself recoverable, which it was not. #845 fixes that first, then applies the rest on top, and adds the ordering fix in add_invoice_action and the removal of a now-unreachable timeout in dev_fee that this branch missed.

@grunch grunch closed this Jul 28, 2026
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.

1 participant