Conversation
initial commit add security considerations new version lud21 fix signature byte order manual remove pdf
LNURLcash, Bearer assetsLNURLcash, Bearer assets
Closes dni#18. WHAT WAS WRONG The README said browser-side integration into lnurl-wallet "is a deliberate follow-up, not part of this repo yet", and listed "No lnurl-wallet integration yet" under known limitations. It exists: device.ts, deviceQueue.ts, deviceOrchestration.ts, DeviceContext.tsx and pages/Vault.tsx, with device.test.ts and deviceOrchestration.test.ts. Checked each file against dni/lnurl-wallet rather than taking the issue's word for it. sdkconfig.defaults opened by saying it had been authored without a working ESP-IDF install, and pointed at a README section called "Status: unverified by compilation" that no longer exists. CI builds both environments on every push and both have run on hardware. The useful half of that note -- that option names drift between IDF releases -- is kept, now next to the fact that platformio.ini pins the platform so it does not happen by surprise. The nvs_keys note said the partition was "now-unused". It is not in the repo at all, and the sentence stopped short of the thing a reader most needs: NVS encryption is off entirely, so a physical flash dump recovers every secret and physical possession is the protection model. Said plainly now. Reported build sizes were 30.1% RAM / 25.6% flash. A real build of this commit is 36.3% / 27.2%. Two claims that hardware has since contradicted: a "What's still unverified" paragraph listing display rendering, button timing, BLE and NVS persistence as open questions, and a line calling export_secret's approval gate "unverified on hardware". All of those have been exercised. That paragraph now defers to docs/HARDWARE-TEST-CHECKLIST.md, which carries dates, boards and firmware versions per area and marks anything unrun as NOT YET BENCH-RUN -- a file is a better authority for this than a prose paragraph that goes stale silently. LINKS THAT DID NOT RESOLVE Seven links pointed at ../luds/25.md or ../../luds/25.md, plus ../lnurl-wallet and ../lnurl-mint. Those only work inside a monorepo checkout; on GitHub they 404. LUD-25 is not a merged file anywhere -- it is an open PR, lnurl/luds#301 -- so the links now point at the proposal, and the two sibling projects at their own repositories. NOT CHANGED The web installer's github.com/dni/lnurl-vault source links. The issue lists them as drift, but they point at the canonical upstream and are correct as written. Deriving them from the Pages host would add JavaScript to a page whose job is flashing firmware, and would not survive a custom domain. Left alone deliberately rather than churned. Verified: 257/257 native assertions; the S3 target builds (the source of the corrected size figures); the LUD-25 PR and all five lnurl-wallet files confirmed to exist via the GitHub API. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RpMeqfvv9KqifKGVM2GwA5
…#18) (#37) Closes #18. **Stacked on #36** — the README now points at `docs/HARDWARE-TEST-CHECKLIST.md`, which that PR adds, so stacking keeps the link from dangling at any point. This PR's own diff is the last commit. ## What was wrong **The wallet integration exists.** The README said it "is a deliberate follow-up, not part of this repo yet" and listed "No `lnurl-wallet` integration yet" under known limitations. `device.ts`, `deviceQueue.ts`, `deviceOrchestration.ts`, `DeviceContext.tsx` and `pages/Vault.tsx` are all there, with `device.test.ts` and `deviceOrchestration.test.ts`. I checked each file against `dni/lnurl-wallet` via the API rather than taking the issue's word for it. **`sdkconfig.defaults` disowned itself.** It opened by saying it had been authored without a working ESP-IDF install, and pointed at a README section called *"Status: unverified by compilation"* that no longer exists. CI builds both environments on every push and both have run on hardware. The useful half of that note — option names drift between IDF releases — is kept, now sitting next to the fact that `platformio.ini` pins the platform so it doesn't happen by surprise. **The `nvs_keys` note undersold the consequence.** It called the partition "now-unused"; it isn't in the repo at all. And it stopped short of what a reader most needs to know: NVS encryption is off entirely, so **a physical flash dump recovers every secret** and physical possession is the protection model. Said plainly now. **Stale numbers.** Reported sizes were 30.1% RAM / 25.6% flash. A real build of this commit is 36.3% / 27.2%. **Two claims hardware has since contradicted:** a "What's still unverified" paragraph listing display rendering, button timing, BLE and NVS persistence as open questions, and a line calling `export_secret`'s approval gate "unverified on hardware". All exercised now. That paragraph defers to the checklist instead — a file with dates, boards and firmware versions per area, which marks anything unrun as `NOT YET BENCH-RUN`, is a better authority than a prose paragraph that goes stale silently. ## Links that did not resolve Seven links pointed at `../luds/25.md` or `../../luds/25.md`, plus `../lnurl-wallet` and `../lnurl-mint`. Those only work inside a monorepo checkout — **on GitHub they 404.** LUD-25 is not a merged file anywhere; it is an open PR, [lnurl/luds#301](lnurl/luds#301). The links now point at the proposal, and the two sibling projects at their own repositories. ## Not changed, deliberately The installer's `github.com/dni/lnurl-vault` source links. The issue lists them as drift, but they point at the canonical upstream and are correct as written. Deriving them from the Pages host would add JavaScript to a page whose job is flashing firmware, and would not survive a custom domain. Say the word if you'd rather they were fork-aware. ## Verification - 257/257 native assertions - the S3 target builds — that build is where the corrected size figures come from - the LUD-25 PR and all five `lnurl-wallet` files confirmed to exist via the GitHub API --------- Co-authored-by: TheCryptoDonkey <TheCryptoDonkey@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implementer use case: TollGate — captive-portal internet access on OpenWrtWe're building TollGate — internet-access gateways on OpenWrt routers. Guests arrive with no internet and no wallet: you can't route a Lightning payment to a device with no network, and you can't hand a Cashu token to someone with no wallet. Bearer notes solve this entirely on frozen LUD-03 — zero draft-spec features needed to ship. Flow (a guest paying with a note — could be an LNbits voucher, a boltcard withdraw link, or any LUD-03
The notes are also directly spendable: they melt straight to any BOLT11 invoice — no e-cash intermediate, no conversion step. A holder can pay any Lightning invoice with a note the same way TollGate does; nothing in the flow above forces creating ecash first. Every step is existing, frozen spec: LUD-03 withdraw callback, issuer pays an invoice. Works today. Where LUD-25 would later help (optional, gated on draft maturity): rotate closes the race window between paste and callback atomically; offline sig verifies paper vouchers by scanning without phoning home; split/merge enables a reseller economy. None of that is on the critical path. We'd be implementer #1 on the consuming side. A few things we hit while reading the draft: (a) PENDING-TTL — no timeout on pending
|
|
hey wow very cool, i though about most of this myself. expiry would be cool and there should be a hardcap on urls. thank you very valid review. |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
LUD-25's wording admits both https://host/w and lnurlw://host/w; the reference stack emits the plain form and that is the intended one. A wallet that fetches the field without LUD-17 translation now works. Raised on lnurl/luds#301. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XHMhjbXkEnnffa1ihqJMFe
LUD-25 states the mint fee as base_fee_msat plus a ppm cut and says
nothing about rounding. Both live implementations read it differently and
both are defensible:
- dni's lnurl-mint, the reference, and what every public mint on the
awesome list except moneyer runs, ceilings the fee to a whole sat on
purpose so the mint is "never short a sat". _mint_fee_msat ends
`return -(-fee_msat // 1000) * 1000`.
- moneyer withholds the msat-exact amount.
So applyMintFee is right about exactly one of them, and a wallet that
compares against it warns spuriously against the other. Measured on real
sats today: 40_000 msat at mint.forgesworn.dev with a 1000 + 1000ppm fee
credited 38_000, not the 38_960 the formula gives, and notecase called
that a discrepancy when the mint had done exactly what it documents.
mintFeeBand gives the range instead: the formula is the most a holder can
be credited, the sat-ceilinged fee the least. withinMintFeeBand is what a
caller should compare a credited note against, rather than an equality
against applyMintFee - an exact match is the best case, not the only
compliant one.
Nothing here decides which reading is correct. That is a question for
lnurl/luds#301; until it settles, both are accepted and anything outside
the band is still wrong.
Co-authored-by: TheCryptoDonkey <TheCryptoDonkey@users.noreply.github.com>
…22) ## What this is An adversarial threat-suite for the bearer-note transport/exposure options currently debated for [LUD-25](lnurl/luds#301) — one executable scenario per scorecard row, so candidate fixes get measured against the same attacks instead of argued about in the abstract. **This PR is documentation, not a fix.** It takes no position on which option wins; it gives the debate a shared, runnable baseline. Fix PRs come later, one per chosen option, each flipping its marked tests. **Language-neutral twin:** the same T1–T11 scorecard now also exists as conformance vectors at [`lnurlcash-conformance` → `vectors/threat-suite.json`](https://github.com/TheCryptoDonkey/lnurlcash-conformance/blob/main/vectors/threat-suite.json), loadable from any implementation's test suite. ## Options under test - **A** — status quo (lnurl/luds#301 as drafted) - **B** — A + comment-secret: WALLET attaches a secret encrypted to the mint on the payRequest (LUD-12 comment); the note's `k1` becomes `<secret>:<preimage>`, and the public LUD-21 preimage alone no longer redeems anything - **C** — "`?p=` everywhere": every `k1` replaced in transport by that `k1` encrypted to the mint - **D** — A + hash-keyed informational GET (poll `/w` by `sha256(k1)`, never `k1`) - **E** — blinded signatures (chaumian model) - **F** — B + D - **G** — locked notes: a second *asset class*, not a bearer variant. The `k1` stays a short plaintext secret, but redemption requires an LUD-04 signature from the LUD-05/LUD-13 `linkingKey` registered at mint/rotate time. Signature-gated — no ciphertext in URLs anywhere. Scored separately below, since the trades differ by note type. ## Scorecard (✓ = attack fails / property holds, ✗ = attack succeeds) | scenario | A | B | C | D | E | F | covered by | |---|:-:|:-:|:-:|:-:|:-:|:-:|---| | T1 verify race | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | `tests/test_poc_verify_race.py` (existing) | | T2 routing-node race (no verify needed) | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | `test_t2_...` (this PR) | | T3 poll-log replay | ✗ | ✗ | ✗ | ✓ | ✗ | ✓ | `test_t3_...` (this PR) | | T4 callback-log replay (control) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | `test_t4_...` (this PR) | | T5 note at rest (bearer axiom, control) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | `test_t5_...` (this PR) | | T6 operator correlation | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | `test_t6_...` (this PR) | | T7 legacy LUD-03 melt | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | melt tests in `test_lnurlcash.py` / `test_verify.py` | | T8 first-contact offline verify | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | analytical — needs `mintPubkey` on record; spec-level gap, no endpoint to hit | | T9 comment silently ignored today | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | `test_t9_...` (this PR) | | T10 merge URL budget | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | `test_t10_...` (this PR) | | T11 offline handoff | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | structural — no endpoint; the bearer property itself (the spec's Offline circulation section) | ## Option G (locked notes) — where it wins, what it costs - **T1/T2 ✓** — the wallet authenticates at `/p/cb`, so the note is locked to its `linkingKey` from birth; a racer holding only `P` cannot redeem. No comment-secret needed for these notes. - **T3/T4 ✓** — a logged `k1` is useless without the key. - **T5 ✓** — the *only* option that beats the at-rest axiom, precisely because it gives up the property the axiom protects. - **T6 ✗** — the operator knows exactly which key owns which notes. - **T7 ✗** — a plain LUD-03 wallet cannot lnurl-auth; no legacy story. - **T11 ✗** — offline handoff dies; transfer requires an online re-lock via the mint. That is the whole price: locked notes are registered claims, not cash. Bearer core (B/D) and locked notes (G) are complements, not competitors — ship the bearer-side race fixes now, spec G as the extension for claim-check use cases. **Why C loses T1/T2/T3:** encrypting to the mint is a *public* operation (`mintPubkey` is advertised), so a racer wraps a leaked preimage himself and replays it; and a logged `p` redeems exactly like a logged `k1` — the mint honors the ciphertext, so the ciphertext *is* the note. Re-encrypting a bearer credential to the party that redeems it never shrinks its exposure set; the only encryption that helps is encrypting to the *holder*, which kills bearer-ness (G takes that trade deliberately, via signatures rather than ciphertext). **Why T2 matters as much as T1:** the race exists even with `VERIFY_ENABLED=false` — every routing node on the mint payment's path learns the preimage as the HTLC settles. Verify only makes the race *public*; the comment-secret is what closes it in both forms. ## Seed-recoverable notes (needs no protocol change beyond D) A WALLET that derives its note secrets deterministically from its seed (BIP32 — reusing LUD-05's own `m/138'`/HMAC(domain) path trick, plus a counter) can restore outstanding notes from the seed alone: re-derive candidates, hash them, look them up by `sha256` — option D doubles as the restore API. Freshly minted notes (`k1` = the mint-generated preimage) are never seed-derived; they live in the wallet's Lightning payment history until rotate-on-receipt converts them into seed-derived ones — the security rule and the backup rule are the same act. **Restore covers device loss, not theft**: anyone who copied a circulating note may have spent it long before the restore runs. (Cashu's NUT-13 already does deterministic secrets — this is parity, not invention.) Pin ONE derivation convention in the spec, or wallets fragment and restores silently miss notes. ## Red/green policy Same convention as `test_melt_restore_double_payout_poc.py`: tests documenting an attack that succeeds **today** assert the current vulnerable behavior and carry `INVERTS WHEN` in their docstring — the PR landing the named option flips them red, forcing the assertions to be rewritten against the fixed behavior. Control tests (T4, T5) pin behavior that must never change. All 223 tests pass as of this PR. ## Implementation notes for the option-B PR (found while writing this suite) - `router._resolve_note`'s `HEX32_PATTERN` rejects any `k1` that isn't 64 lowercase hex — a composite `<secret>:<preimage>` `k1` dies at the door. That PR must relax the pattern (NoteStore ids stay `sha256` over the composite's raw bytes, so payment-hash-keyed mints keep working). - `router.py`'s own docstrings already cite "the spec's Security considerations" for the rotate-immediately rule — a section LUD-25 does not yet contain. This suite is that section's executable skeleton. - Encryption key management, resolved in discussion: the mint derives its encryption keypair from the `signmessage` RPC it already uses (LUD-13's trick: a canonical, never-signable-on-request phrase, `enc_priv = sha256(sig)`), so no raw-ECDH RPC (LND `DeriveSharedKey` / CLN `getsharedsecret`) is needed. The derived pubkey ≠ node id, so it must be published at payRequest time (next to `withdrawLink`) — the comment rides the callback *request*, before any invoice exists. Decrypt at `/p/cb` time, store only `sha256(secret:P)`, never the ciphertext. ## One unrelated change, called out rather than buried `flake.nix` drops `-q` from three `curl | grep` checks in the NixOS VM test. Nothing to do with bearer notes, and happy to split it out if you would rather. `grep -q` exits on the first match and closes the pipe, `curl` then dies of `SIGPIPE` (exit 23), and the driver's `pipefail` turns a *passing* match into a failing step. It only bites when the response is large enough that curl is still writing when grep exits, which is why it reads as an intermittent CI flake rather than a broken assertion. Dropping `-q` costs a line of log output and makes the check deterministic. --------- Co-authored-by: TheCryptoDonkey <TheCryptoDonkey@users.noreply.github.com>
|
Re-read the document at fc1296c against my comments above, since text has landed since I wrote them. Where each stands: Addressed — closing from my side:
Still open:
Separately, a couple of items on the seed-derivation section and the |
This comment was marked as resolved.
This comment was marked as resolved.
|
@TheCryptoDonkey updated to lud and addresses most of your issues. |
|
LFG!!!! |
|
added @TheCryptoDonkey retry rule for mutation requests |
LNURL-withdraw (LUD-03) links are redeemable by anyone who holds a valid
k1. This document formalizes that property into a bearer-asset scheme: ak1is no longer only a one-time ticket that gets exchanged for a BOLT-11 invoice, it can itself be the asset. Ak1that aSERVICEhas credited with value can be copied, printed, NFC-tapped or handed over exactly like a banknote, and can be re-issued ("melted and re-minted"), merged with other bearer notes, or split into change, all without the holder needing to be online or to have a route back toSERVICEat handoff time.Because a bearer
k1is nothing more than a normalwithdrawRequestk1,LNURLcashrequires no new endpoint and no new encoding. ASERVICEthat implements it is still speaking plain LUD-03; aWALLETthat does not know aboutLNURLcashstill sees a normal withdraw link and can cash it out to a BOLT-11 invoice as usual.LNURLcashis therefore fully optional and backward-compatible.projects
i am currently working on a lnurl-wallet and lnurl-mint. passed alpha now in beta testing