Add L402 discovery: manifest + quote endpoints#241
Conversation
In this commit, we factor the core of MintL402 (identifier creation, secret generation, macaroon assembly, and transaction logging) into a shared mintWithChallenge helper, and expose a new MintL402WithCaveats method on top of it. Where MintL402 derives its caveats from the configured ServiceLimiter, MintL402WithCaveats takes an explicit price and caveat set from the caller. This is what the L402 discovery quote endpoint needs: it lets us mint a credential for a client-chosen bundle while reusing the same identifier, secret, and challenge machinery, so the result is indistinguishable from a reactive challenge at verification time.
In this commit, we add a formula pricing model: a base price plus per-unit components that scale with a client's chosen constraint values, computed as basemsat + sum(pricemsatperunit * ceil(value / unit)). The full price is computable by a client from the discovery manifest alone, while the invoice the server issues stays authoritative. The config lives on proxy.Service as a value struct mirroring DynamicPrice, and is surfaced only through the L402 discovery layer. It does not change the reactive 402 price, which still uses Price or DynamicPrice. We also expose Service.ResourcePrice so callers outside the proxy package (the discovery quote endpoint) can query the service's configured pricer.
In this commit, we add the server side of the L402 discovery layer. The new package serves a free static manifest at /.well-known/l402.json that advertises the configured services, their tiers, capabilities, prices (fixed, formula, or dynamic), constraint bounds, and the caveat vocabulary the proxy enforces. It also serves an optional quote endpoint at /l402/quote. A client proposes a bundle (service, capabilities, constraint values, optional budget), and we price it, map it to the macaroon's first-party caveats, and mint a ready-to-pay challenge via MintL402WithCaveats. Because the quote is an ordinary L402 challenge, the credential verifies through the unmodified base path. Both handlers are free, send permissive CORS headers, and return a structured error envelope on failure.
In this commit, we wire the discovery manifest and quote endpoint into the proxy as priority local services, the same mechanism the admin and dashboard endpoints use, so they bypass the 402 gate and are always available. They reuse the same minter as the reactive flow. The sample config documents the new formula pricing option.
|
Nice to see the server side land — read through the discovery package and the mint refactor. One thing in the formula pricing path I'd flag before it ships, plus two smaller notes. Formula price can overflow int64 and underprice the quote
units := (value + unit - 1) / unit
price += c.PriceMsatPerUnit * unitsThe only upper-bound check in if cfg, ok := svc.Constraints[cond]; ok {
if max, err := ...; n > max { /* out of bounds */ }
}A formula-only constraint (in Concretely: a Two things stack here:
The reactive 402 path never had this exposure since Suggested: require each formula component's constraint to resolve to a bounded one (checked at config load), do the price math with checked/saturating arithmetic, and reject a non-positive final price before minting. Smaller
Happy to be wrong if I've misread the bounds flow — that's the part I traced hardest. |
In this PR, we add the server side of the L402 discovery layer to aperture, as
specified in lightninglabs/L402#27.
Today aperture is reactive: a client only learns what a service costs by hitting
it and getting a 402 back. Discovery lets a client see the catalog, the prices,
and the caveat vocabulary up front, and gives us a place for dynamic pricing and
the start of in-protocol negotiation.
Two new endpoints are mounted as priority local services, the same mechanism the
admin and dashboard endpoints use, so they bypass the 402 gate and are always
free.
/.well-known/l402.jsonserves a static manifest built from theconfigured services: their capabilities, tiers, prices, constraint bounds, and
the caveat conditions the proxy enforces.
/l402/quotetakes a client-chosenbundle (service, capabilities, constraint values, optional budget), prices it,
maps it to the macaroon's first-party caveats, and mints a ready-to-pay
challenge.
No new verification machinery
The quote endpoint reuses the existing mint and challenger. We factor the core
of
MintL402into a shared helper and addMintL402WithCaveats, which takes anexplicit price and caveat set instead of deriving them from the static
ServiceLimiter. A quote response is just an ordinary L402 challenge minted fora bundle the client chose, so the credential verifies through the unmodified
base path: nothing downstream of the mint changes.
Formula pricing
We add a formula pricing model (
basemsat + sum(pricemsatperunit * ceil(value / unit))) as a value struct onproxy.Service, mirroringDynamicPrice. It issurfaced only through discovery: the manifest advertises it, and the quote
endpoint computes the price from the client's chosen constraint values. The
reactive 402 price is untouched and still uses
PriceorDynamicPrice. Thesample config documents the new option.
The quote endpoint returns a structured error envelope on failure, sends
permissive CORS headers (the manifest carries no secrets), and the discovery
package ships with unit tests covering the manifest build, the pricing models,
the bundle-to-caveat mapping, and the error cases.
BOLT 12 is intentionally not wired here: a macaroon must commit to the payment
hash, which a BOLT 12 offer does not expose until an invoice is fetched, so that
binding needs its own treatment, as noted in the spec.