-
Notifications
You must be signed in to change notification settings - Fork 48
feat: BOLT12 offer payouts via LNDK and BIP-353 DNS resolution #720
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
QaidVoid
wants to merge
2
commits into
MostroP2P:main
Choose a base branch
from
QaidVoid:feat/bolt12-lndk
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| # BOLT12 Offer Payouts via LNDK | ||
|
|
||
| Mostro optionally supports paying buyer payouts to BOLT12 offers (`lno1…`) by | ||
| talking to an [LNDK](https://github.com/lndk-org/lndk) daemon that runs | ||
| alongside LND. This page documents the operator setup. | ||
|
|
||
| > **Experimental.** BOLT12, LND's onion messaging support, and LNDK itself | ||
| > are all still maturing. Leave `lndk_enabled = false` unless you understand | ||
| > the risks and want to opt in. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| Buyer ---- Nostr ----> Mostro ----gRPC----> LNDK ----gRPC----> LND | ||
| (sends lno1…) | | | | ||
| | | | | ||
| +--BOLT11/LNURL path---|------gRPC--------->+ | ||
| | | ||
| +- onion messaging -+ | ||
| (via LND custom | ||
| message 513) | ||
| ``` | ||
|
|
||
| - LND handles everything Mostro always did: hold invoices, routing, channels. | ||
| - LNDK is a thin shim that uses LDK's BOLT12 implementation to build | ||
| `invoice_request` messages, fetch invoices from offer issuers, and hand | ||
| payable invoices back to LND. | ||
| - Mostro calls LNDK's gRPC only for the buyer-payout step when | ||
| `order.buyer_invoice` is a BOLT12 offer. All other payment paths are | ||
| untouched. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| ### LND ≥ v0.18.0 with the right build tags | ||
|
|
||
| Build (or install a package built) with the subservers LNDK needs: | ||
|
|
||
| ```sh | ||
| make install tags="peersrpc signrpc walletrpc" | ||
| ``` | ||
|
|
||
| ### LND startup flags | ||
|
|
||
| LND must advertise and forward onion messages. Add to `lnd.conf`: | ||
|
|
||
| ``` | ||
| [protocol] | ||
| protocol.custom-message=513 | ||
| protocol.custom-nodeann=39 | ||
| protocol.custom-init=39 | ||
| ``` | ||
|
|
||
| Or pass them on the command line: | ||
|
|
||
| ```sh | ||
| lnd --protocol.custom-message=513 \ | ||
| --protocol.custom-nodeann=39 \ | ||
| --protocol.custom-init=39 | ||
| ``` | ||
|
|
||
| On startup Mostro checks the LND feature bits and logs a warning if these | ||
| flags appear to be missing. | ||
|
|
||
| ### Install and run LNDK | ||
|
|
||
| Follow LNDK's own setup guide at | ||
| <https://github.com/lndk-org/lndk#setting-up-lndk>. In short: | ||
|
|
||
| ```sh | ||
| git clone https://github.com/lndk-org/lndk | ||
| cd lndk | ||
| cargo run --bin=lndk -- \ | ||
| --address=https://127.0.0.1:10009 \ | ||
| --cert-path=/path/to/.lnd/tls.cert \ | ||
| --macaroon-path=/path/to/.lnd/data/chain/bitcoin/mainnet/admin.macaroon | ||
| ``` | ||
|
|
||
| By default LNDK writes its self-signed TLS cert to `~/.lndk/data/tls-cert.pem` | ||
| and listens on `https://127.0.0.1:7000`. | ||
|
|
||
| ### Bake a custom macaroon for Mostro | ||
|
|
||
| Use a minimally scoped macaroon instead of `admin.macaroon`: | ||
|
|
||
| ```sh | ||
| lncli bakemacaroon --save_to=/path/to/mostro-lndk.macaroon \ | ||
| uri:/walletrpc.WalletKit/DeriveKey \ | ||
| uri:/signrpc.Signer/SignMessage \ | ||
| uri:/lnrpc.Lightning/GetNodeInfo \ | ||
| uri:/lnrpc.Lightning/ConnectPeer \ | ||
| uri:/lnrpc.Lightning/GetInfo \ | ||
| uri:/lnrpc.Lightning/ListPeers \ | ||
| uri:/lnrpc.Lightning/GetChanInfo \ | ||
| uri:/lnrpc.Lightning/QueryRoutes \ | ||
| uri:/routerrpc.Router/SendToRouteV2 \ | ||
| uri:/routerrpc.Router/TrackPaymentV2 | ||
| ``` | ||
|
|
||
| ## Mostro configuration | ||
|
|
||
| In `settings.toml`, fill in the `[lightning]` LNDK fields: | ||
|
|
||
| ```toml | ||
| [lightning] | ||
| # ... existing LND settings ... | ||
|
|
||
| lndk_enabled = true | ||
| lndk_grpc_host = "https://127.0.0.1:7000" | ||
| lndk_cert_file = "/home/mostro/.lndk/data/tls-cert.pem" | ||
| lndk_macaroon_file = "/home/mostro/mostro-lndk.macaroon" | ||
| lndk_fetch_invoice_timeout = 60 | ||
| # lndk_fee_limit_percent = 0.2 # fraction (0.002 = 0.2%). Defaults to mostro.max_routing_fee. | ||
| ``` | ||
|
|
||
| On startup Mostro will: | ||
|
|
||
| 1. Try to dial LNDK. If it cannot (wrong cert, unreachable, bad macaroon), | ||
| startup aborts — BOLT12 is opt-in, so silently dropping it is worse than | ||
| refusing to start. | ||
| 2. Log whether LND's onion-message feature bits are advertised. If not, a | ||
| loud warning is emitted but startup continues. | ||
|
|
||
| ## What Mostro does with an offer | ||
|
|
||
| When a buyer sends a BOLT12 offer string as their payout destination: | ||
|
|
||
| 1. **Validation (at `add-invoice` / `take-sell` time).** `is_valid_invoice` | ||
| decodes the offer with the `lightning` crate and rejects: | ||
| - non-BTC currency offers (e.g. USD); | ||
| - offers whose pinned amount disagrees with `order.amount - order.fee`; | ||
| - offers whose `absolute_expiry` has already elapsed; | ||
| - offers that cannot satisfy a single-item purchase; | ||
| - offers received while `lndk_enabled = false`. | ||
| 2. **Payout (`do_payment`).** Mostro calls LNDK's `GetInvoice` to fetch a | ||
| fresh BOLT12 invoice bound to the offer, **re-validates** the fetched | ||
| invoice's amount and expiry (LNDK's `PayOffer` shortcut does not), then | ||
| calls `PayInvoice`. On success the returned preimage transitions the | ||
| order to `Success`. On failure the order enters the normal failed-payment | ||
| retry loop. | ||
|
|
||
| ## BIP-353 resolution (`user@domain` → BOLT12 offer) | ||
|
|
||
| When `bip353_enabled = true`, Mostro resolves human-readable | ||
| `user@domain.tld` payout targets to BOLT12 offers via DNSSEC-validated DNS | ||
| TXT records (BIP-353 / `_bitcoin-payment` zone). On a successful resolve, | ||
| the original address is replaced with the resolved offer at order creation | ||
| time and the BOLT12 payout path described above takes over. On any failure | ||
| (no record, DNSSEC fails, malformed URI), Mostro falls back to the LNURL | ||
| path so existing Lightning Addresses keep working. | ||
|
|
||
| Configuration: | ||
|
|
||
| ```toml | ||
| [lightning] | ||
| bip353_enabled = true | ||
| # Any DoH resolver supporting RFC 8484's JSON API works. Default below. | ||
| bip353_doh_resolver = "https://1.1.1.1/dns-query" | ||
| # Skip DNSSEC AD-flag check. DANGER: regtest only. | ||
| bip353_skip_dnssec = false | ||
| ``` | ||
|
|
||
| Notes: | ||
|
|
||
| - BIP-353 requires `lndk_enabled = true`; otherwise resolution is skipped | ||
| silently because the resolved offer would be unpayable. | ||
| - DNSSEC validation is enforced via the resolver's `AD` flag. Disabling | ||
| `bip353_skip_dnssec` in production lets DNS-level attackers redirect | ||
| payouts. | ||
| - Resolution is best-effort: a DoH timeout or non-DNSSEC response is | ||
| treated as "no record" so the LNURL path can still serve the request. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **BOLT12 invoices (`lni1…`) as direct inputs are rejected.** Users must | ||
| send the offer, not a pre-fetched invoice. | ||
| - **Offer creation is not supported.** Mostro does not issue BOLT12 offers | ||
| for dev-fee receipt; dev fees still use a BOLT11 destination from config. | ||
| - **No background retries for BOLT12 yet.** Offer reusability makes this | ||
| trivial to add in a follow-up, but for now BOLT12 payment failures follow | ||
| the same retry cadence as BOLT11 and still surface an `AddInvoice` | ||
| request to the buyer after the configured retry budget is exhausted. | ||
| - **Onion-message network reachability is still maturing.** BOLT12 fetches | ||
| can fail in ways BOLT11 does not — check Mostro logs for `lndk | ||
| get_invoice:` errors if you see unexpected BOLT12 failures. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| | Symptom | Likely cause | | ||
| |---|---| | ||
| | `LNDK initialization failed: failed to read LNDK TLS cert` | `lndk_cert_file` path wrong or file not readable by the Mostro user | | ||
| | `LNDK initialization failed: failed to connect to LNDK` | LNDK daemon not running, or `lndk_grpc_host` mismatch | | ||
| | `LNDK initialization failed: TLS config` | Cert file is not a valid PEM certificate | | ||
| | Warning: `LND does not advertise onion-message support` | Missing `--protocol.custom-message=513 --protocol.custom-nodeann=39 --protocol.custom-init=39` on LND | | ||
| | `lndk get_invoice: ...` errors in logs during payout | Offer issuer unreachable, network has no onion-message route, or the offer expired | | ||
| | `BOLT12 invoice amount mismatch` | The offer issuer returned an invoice that does not match the requested amount — defense-in-depth aborted the payment | | ||
|
|
||
| ## Disabling BOLT12 | ||
|
|
||
| Set `lndk_enabled = false` and restart. Existing orders whose | ||
| `buyer_invoice` is a BOLT12 offer will fail their next payout attempt with | ||
| `BOLT12 offer received but LNDK is disabled` and enter the usual retry | ||
| loop. Consider waiting until all in-flight BOLT12 orders drain before | ||
| flipping the flag. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Add language specifiers to fenced code blocks (MD040).
Static analysis (markdownlint MD040) flagged two fenced blocks without a language. As per coding guidelines: "Add a language specifier to every fenced code block in documentation to comply with markdownlint MD040".
📝 Proposed fix
Also applies to: 46-46
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents