lnurlcash-conformance 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +18 -38
- package/mock-mint/index.d.ts +7 -13
- package/mock-mint/index.mjs +155 -29
- package/package.json +1 -1
- package/runner/index.mjs +261 -78
- package/vectors/lifecycle.json +6 -5
- package/vectors/mint-to-hash.json +50 -32
- package/vectors/pay-request.json +51 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,97 @@ Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
|
4
4
|
may add or tighten checks that a previously-passing mint now fails; pin an
|
|
5
5
|
exact version if you gate CI on the grade.
|
|
6
6
|
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
## 0.5.0 - 2026-08-31
|
|
10
|
+
|
|
11
|
+
**Breaking: the current LUD-25 draft's mandatory mint comment is now the
|
|
12
|
+
baseline.** Every minting payRequest must advertise `commentAllowed >= 64`,
|
|
13
|
+
and every quote must carry `comment=hex(sha256(secret))`; missing or malformed
|
|
14
|
+
comments are refused before invoice creation. The payment preimage is
|
|
15
|
+
settlement proof and never a bearer credential. `mintToHash`/`h` remains an
|
|
16
|
+
additive ForgeSworn compatibility and receipt extension: when used, `h` must
|
|
17
|
+
repeat the mandatory comment and cannot replace it. The mock, runner,
|
|
18
|
+
lifecycle and pay-request vectors all enforce the same rule, including a
|
|
19
|
+
mismatched `h`/`comment` refusal. `commentFallsBack` remains only as the
|
|
20
|
+
non-compliant historical fixture documented in
|
|
21
|
+
`docs/COMMENT-IS-MANDATORY.md`.
|
|
22
|
+
|
|
23
|
+
**A mint that is not a Lightning Address is no longer failed for having no
|
|
24
|
+
mint address document.** The document is probed by swapping
|
|
25
|
+
`/.well-known/lnurlp/` for `/.well-known/lnurlw/` in the payRequest URL. On a
|
|
26
|
+
mint served from a plain path that swap changes nothing, so the probe
|
|
27
|
+
re-fetched the payRequest, read `tag: "payRequest"`, and failed the mint for
|
|
28
|
+
an optional document it has nowhere to publish. Found by grading
|
|
29
|
+
`bitkarrot/lnurlmint`, which is served at `/lnurlmint/lnurlp/<id>`; it now
|
|
30
|
+
grades 6 passed, 0 failed on the checks that predate the mandate above.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
**Two checks for the draft's 25 August additions, both optional-graded.**
|
|
34
|
+
|
|
35
|
+
`answers a note lookup by hash without the secret` covers "Checking a note
|
|
36
|
+
without exposing it". The capability is detected rather than announced: the
|
|
37
|
+
draft deliberately gives an unrecognized `h` the same answer an unknown
|
|
38
|
+
`k1` gets, so a live note's own hash is the only probe that separates a
|
|
39
|
+
mint which implements the lookup from one which never did. A mint that does
|
|
40
|
+
not offer it is not graded down; one that offers it must omit `k1` from the
|
|
41
|
+
reply - a wallet asking by hash already holds the secret, and filling the
|
|
42
|
+
field in puts the note back on the wire the lookup exists to keep it off -
|
|
43
|
+
must report the same value by hash as by `k1`, and must refuse a hash it
|
|
44
|
+
never registered.
|
|
45
|
+
|
|
46
|
+
`refuses an oversized merge cleanly` covers the merge cap. The cap itself
|
|
47
|
+
is a MAY, so a mint that does not name one is not graded down, only
|
|
48
|
+
reported, since a wallet must then batch by URL length rather than rely on
|
|
49
|
+
a refusal. The one answer that fails is `OK`: every input on that probe is
|
|
50
|
+
fabricated by the runner, so a mint accepting it has minted an output
|
|
51
|
+
against notes it never held, which is what a truncated `k1` list read as a
|
|
52
|
+
shorter merge looks like from outside.
|
|
53
|
+
|
|
54
|
+
## 0.4.0 - 2026-08-26
|
|
55
|
+
|
|
56
|
+
**The mint-output naming check now reads the spelling LUD-25 actually
|
|
57
|
+
specifies.** The draft names the output with a LUD-12 `comment = hex(h)`,
|
|
58
|
+
advertised as a `commentAllowed` of at least 64; `mintToHash` is the
|
|
59
|
+
parameter form one mint shipped before the comment form was written. The
|
|
60
|
+
suite knew only the latter, so a mint naming notes exactly as the draft
|
|
61
|
+
describes graded as "not offered - a minted note's k1 is the invoice
|
|
62
|
+
preimage". That is the worst kind of wrong answer a conformance suite can
|
|
63
|
+
give: it tells a wallet author the safe mint is the unsafe one.
|
|
64
|
+
|
|
65
|
+
Both spellings are now read from the payRequest and the mint address
|
|
66
|
+
document, probed with a fresh hash each (a mint that binds output ids
|
|
67
|
+
uniquely would rightly refuse the second spelling for naming an output the
|
|
68
|
+
first just took), and reported as `named by comment` / `named by h`.
|
|
69
|
+
|
|
70
|
+
The two are deliberately not probed identically, because the draft's rules
|
|
71
|
+
for them are opposites:
|
|
72
|
+
|
|
73
|
+
- `h` is a parameter invented for naming, so a malformed one MUST be
|
|
74
|
+
refused before an invoice exists. Unchanged.
|
|
75
|
+
- `comment` is plain LUD-12 free text any wallet may send for unrelated
|
|
76
|
+
reasons, so a comment that is not a bare hash MUST fall back to crediting
|
|
77
|
+
`k1=P` rather than be refused, and the mint MUST NOT serve LUD-21 `verify`
|
|
78
|
+
on that fallback - there the preimage is not proof of payment, it is the
|
|
79
|
+
note. Both are now checked, the second as a failure.
|
|
80
|
+
|
|
81
|
+
Only `mintToHash` is expected in both documents. `commentAllowed` is a
|
|
82
|
+
payRequest field; the mint address document is a withdrawRequest, where a
|
|
83
|
+
LUD-12 comment has nowhere to go, so its absence there is no longer
|
|
84
|
+
reported as a disagreement.
|
|
85
|
+
|
|
86
|
+
**A rate-limited grade no longer accuses the mint.** Every mint quote
|
|
87
|
+
issues a real invoice on a real node, so a grader firing a dozen looks like
|
|
88
|
+
the abuse a limiter exists to stop - and probing two spellings nearly
|
|
89
|
+
doubled the count. HTTP 429 was indistinguishable from a spec refusal, so
|
|
90
|
+
the mint was reported as violating whatever check happened to be running
|
|
91
|
+
when the bucket ran dry. 429 is now waited out (honouring `Retry-After`)
|
|
92
|
+
and retried, and reported as an incomplete grade rather than a verdict if
|
|
93
|
+
it persists.
|
|
94
|
+
|
|
95
|
+
The mock mint gains `commentAllowed`, `commentRefusesMalformed` and
|
|
96
|
+
`verifyOnUnnamedMint` to cover all of this.
|
|
97
|
+
|
|
7
98
|
## 0.3.0 - 2026-08-24
|
|
8
99
|
|
|
9
100
|
- Add an optional bound-mint settlement receipt for sealed signers. A
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ for (const c of cases) {
|
|
|
60
60
|
| `payment-request.json` | `lnurlcashreq1`: one holder asking another for value |
|
|
61
61
|
| `settle-for-value.json` | the decision table a server works through to take a note as payment |
|
|
62
62
|
| `retried-mutation.json` | what makes a repeated mutation a retry rather than a double-spend |
|
|
63
|
-
| `mint-to-hash.json` |
|
|
63
|
+
| `mint-to-hash.json` | additive `mintToHash` compatibility and optional bound LUD-21 receipts; not baseline LUD-25 |
|
|
64
64
|
| `lifecycle.json` | behavioural requirements, as scenarios to drive |
|
|
65
65
|
| `threat-suite.json` | the transport/exposure scorecard — candidate spec options against fixed attacks (non-normative) |
|
|
66
66
|
|
|
@@ -100,10 +100,10 @@ must survive:
|
|
|
100
100
|
| `--sunset` | refuses anything that grows its liabilities |
|
|
101
101
|
| `--baseFeeMsat=N --feePpm=N` | advertises and withholds a mint fee |
|
|
102
102
|
| `--roundFeeToSat` | rounds the withheld fee up to a whole sat — the note mints short of the formula |
|
|
103
|
-
| `--verifyLeaksEarly` | serves
|
|
103
|
+
| `--verifyLeaksEarly` | serves a preimage before settlement, falsely claiming payment proof before payment happened |
|
|
104
104
|
| `--mintToHashAcceptsMalformedH` | claims `mintToHash` and invoices an `h` that is not 64 lowercase hex, so a wallet pays for a quote the mint will refuse |
|
|
105
105
|
| `--mintToHashAcceptsUsedH` | claims it and invoices an `h` that already names a note, an invoice or another quote's output |
|
|
106
|
-
| `--mintToHashIgnoresH` | claims it
|
|
106
|
+
| `--mintToHashIgnoresH` | claims it but accepts `h` and mandatory `comment` naming different outputs |
|
|
107
107
|
|
|
108
108
|
The three `mintToHash*` misbehaviours need `--mintToHash` alongside them;
|
|
109
109
|
on their own they do nothing, because a mint that never offered the
|
|
@@ -126,7 +126,7 @@ answered:
|
|
|
126
126
|
| `--previousPrivateKey=<hex>` | an old signing key the mock still holds. Its public half joins `previousPubkeys` on its own |
|
|
127
127
|
| `--signWithPreviousKey` | issues every note under that old key while still advertising the new one: the mid-rotation state a mint passes through when the advertisement moves before the signer |
|
|
128
128
|
| `--retriedMutation=replay` | answers a byte-identical repeat of a mutation with the original success instead of `already spent`. The default, `refuse`, is what this mock has always done |
|
|
129
|
-
| `--mintToHash` |
|
|
129
|
+
| `--mintToHash` | accepts `h` alongside the mandatory identical comment and enables the additive quote/receipt fields. Off by default; baseline comment-bound minting remains on |
|
|
130
130
|
| `--mintReceipt` | with `--mintToHash`, adds the optional quote commitment and signed LUD-21 settlement receipt |
|
|
131
131
|
| `--mintToHashAdvertisedOn=quote` | narrows which of the three places claim it (`payRequest`, `mintAddress`, `quote`); all three by default. Changes only what is claimed, never what the mint does |
|
|
132
132
|
|
|
@@ -155,43 +155,23 @@ npx lnurlcash-conform mint@example.com
|
|
|
155
155
|
|
|
156
156
|
Read-only by default: resolves the payRequest, checks the `withdrawLink`
|
|
157
157
|
(either legal spelling, and the report says which one the mint uses),
|
|
158
|
-
the fee advertisement, invoice amounts,
|
|
159
|
-
|
|
160
|
-
everyone on the payment's route knows the payment hash), whether an
|
|
158
|
+
the fee advertisement, invoice amounts, mandatory `commentAllowed: 64`,
|
|
159
|
+
pre-invoice rejection of missing or malformed mint comments, whether an
|
|
161
160
|
unknown note is reported distinguishably from a spent one, and the
|
|
162
161
|
experimental mint address.
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
money moves, because the other two can be cached). Anything that is not
|
|
177
|
-
exactly the boolean `true` is no.
|
|
178
|
-
|
|
179
|
-
A mint that says nothing anywhere is reported as not offering it and passes,
|
|
180
|
-
which is every mint today. A mint that claims it is asked to prove the
|
|
181
|
-
refusals: a malformed `h` must get no invoice at all, since a wallet that
|
|
182
|
-
pays for a quote the mint will reject has bought nothing. Malformed means
|
|
183
|
-
not 32 bytes of hex, in any casing. A wallet MUST send `h` as 64 lowercase
|
|
184
|
-
hex and every client here does, but hex is case-insensitive, so a service
|
|
185
|
-
SHOULD normalise before comparing and MUST NOT read `AAAA...` and `aaaa...`
|
|
186
|
-
as two different outputs: keying the string it was handed files the note
|
|
187
|
-
where the wallet will never look for it, and nobody is told. A service that
|
|
188
|
-
refuses upper case outright is being strict rather than wrong, so the grader
|
|
189
|
-
does not probe it either way. Where the three
|
|
190
|
-
claims disagree, the grader names the disagreement rather than failing it:
|
|
191
|
-
none of those loses anyone money on its own. What is failed is a mint that
|
|
192
|
-
claims the capability and does not bind, because a wallet believing the
|
|
193
|
-
claim stops rotating on sight; that one needs a settlement to see, so it
|
|
194
|
-
rides on `--preimage`.
|
|
163
|
+
Current LUD-25 minting is always comment-bound. The wallet persists a secret,
|
|
164
|
+
sends `comment=hex(sha256(secret))`, and the payment preimage remains ordinary
|
|
165
|
+
settlement proof. A mint that cannot accept the 64-character commitment, or
|
|
166
|
+
that silently creates a preimage-backed note, fails grading.
|
|
167
|
+
|
|
168
|
+
`mintToHash` is retained as an additive compatibility field. When advertised,
|
|
169
|
+
the runner sends `h` alongside the mandatory comment and requires both to name
|
|
170
|
+
the same output. A malformed `h` must be rejected before invoice creation.
|
|
171
|
+
The payRequest, experimental mint-address document and quote echo are checked
|
|
172
|
+
separately because each claim has a different lifetime. Disagreement between
|
|
173
|
+
those extension advertisements warns; failure to honour a claimed binding
|
|
174
|
+
fails when the paid `--preimage` check proves where the note actually landed.
|
|
195
175
|
|
|
196
176
|
Three other things a mint may publish are graded softly, because none of them
|
|
197
177
|
is in LUD-25: the mint info on the discovery endpoint, a `/stats` endpoint
|
package/mock-mint/index.d.ts
CHANGED
|
@@ -137,14 +137,10 @@ export interface MockMintOptions {
|
|
|
137
137
|
/** what the node behind a stats-publishing mock claims to hold */
|
|
138
138
|
localBalanceMsat?: number
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* thing `h` means on the withdraw callback - and credits the note at
|
|
145
|
-
* that id when the invoice settles. The payment preimage is then not a
|
|
146
|
-
* valid k1 for that note, which matters because every routing node on
|
|
147
|
-
* the payment path learns it, and so does anyone who saw the invoice.
|
|
140
|
+
* Additive compatibility spelling for the mandatory mint comment. Off,
|
|
141
|
+
* `h` is not advertised or read, while comment-bound minting remains the
|
|
142
|
+
* baseline. On, WALLET may repeat the same 64-hex output commitment as
|
|
143
|
+
* `h`, enabling the ForgeSworn/Moneyer quote and receipt vocabulary.
|
|
148
144
|
*
|
|
149
145
|
* The capability is advertised in three places: `mintToHash: true` on
|
|
150
146
|
* the payRequest (every mint has one, so it is what a wallet decides
|
|
@@ -176,11 +172,9 @@ export interface MockMintOptions {
|
|
|
176
172
|
*/
|
|
177
173
|
mintToHashAcceptsUsedH?: boolean
|
|
178
174
|
/**
|
|
179
|
-
* non-compliant, mintToHash on:
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* nothing. The wallet stopped rotating on sight because it was told it
|
|
183
|
-
* did not need to, so this is the worst of both schemes.
|
|
175
|
+
* non-compliant, mintToHash on: accept `h` and mandatory `comment` even
|
|
176
|
+
* when they name different outputs. The normative comment still binds
|
|
177
|
+
* the note, but the extension claim and any h-watching signer disagree.
|
|
184
178
|
*/
|
|
185
179
|
mintToHashIgnoresH?: boolean
|
|
186
180
|
/**
|
package/mock-mint/index.mjs
CHANGED
|
@@ -66,9 +66,8 @@ const DEFAULTS = {
|
|
|
66
66
|
// WALLET that handles one but not the other fails against real mints.
|
|
67
67
|
// Run your client against both.
|
|
68
68
|
withdrawLinkForm: 'plain',
|
|
69
|
-
// LUD-21 verify endpoint. Off means 404, not merely unadvertised
|
|
70
|
-
//
|
|
71
|
-
// off switch.
|
|
69
|
+
// LUD-21 verify endpoint. Off means 404, not merely unadvertised, so the
|
|
70
|
+
// mock can exercise current mints that provide no settlement polling.
|
|
72
71
|
verify: true,
|
|
73
72
|
// Publish `payLink` on a note's informational GET, the way home for a
|
|
74
73
|
// holder who has nothing but the note. On by default because the
|
|
@@ -166,17 +165,14 @@ const DEFAULTS = {
|
|
|
166
165
|
//
|
|
167
166
|
// ---- naming the note you are buying ----
|
|
168
167
|
//
|
|
169
|
-
// Off
|
|
170
|
-
//
|
|
171
|
-
//
|
|
168
|
+
// Off means only the additive `h` spelling is absent: it is not advertised
|
|
169
|
+
// and the pay callback does not read it. Mandatory comment-bound minting
|
|
170
|
+
// remains active independently.
|
|
172
171
|
//
|
|
173
|
-
// On, a WALLET MAY
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// Which matters because two sets of untrusted people learn a preimage:
|
|
178
|
-
// every routing node on the payment path, and anyone who merely saw the
|
|
179
|
-
// invoice and polled /verify with its payment hash.
|
|
172
|
+
// On, a WALLET MAY repeat the mandatory comment hash as
|
|
173
|
+
// h=<64 lowercase hex> on the LUD-06 pay callback. This enables the
|
|
174
|
+
// earlier ForgeSworn/Moneyer capability and receipt vocabulary without
|
|
175
|
+
// changing which output the normative comment names.
|
|
180
176
|
//
|
|
181
177
|
// The capability is claimed in three places and they say different
|
|
182
178
|
// things. `mintToHash: true` on the payRequest means "I accept an h on
|
|
@@ -187,6 +183,23 @@ const DEFAULTS = {
|
|
|
187
183
|
// which is the one that matters at the moment money moves: the other
|
|
188
184
|
// two can be cached or stale.
|
|
189
185
|
mintToHash: false,
|
|
186
|
+
// LUD-25 "Checking a note without exposing it": answer the informational
|
|
187
|
+
// GET by `?h=sha256(k1)` as well as by `?k1=`, so a wallet can look a note
|
|
188
|
+
// up without putting the live secret in a query string. Optional.
|
|
189
|
+
// true - compliant
|
|
190
|
+
// 'echoesK1' - non-compliant: fills in k1, putting the secret back
|
|
191
|
+
// on the wire the lookup existed to keep it off
|
|
192
|
+
// 'answersUnknown' - non-compliant: answers for a hash it never
|
|
193
|
+
// registered, instead of the unknown-note refusal
|
|
194
|
+
hashLookup: false,
|
|
195
|
+
// LUD-25 lets a SERVICE refuse an oversized merge outright rather than
|
|
196
|
+
// let the URL be mangled upstream. 0 is no explicit cap; a positive number
|
|
197
|
+
// refuses more than that many k1 with the draft's own reason string.
|
|
198
|
+
mergeCap: 0,
|
|
199
|
+
// non-compliant: answer OK to an oversized merge whatever its inputs -
|
|
200
|
+
// what a mint that parses a truncated k1 list and shrugs looks like from
|
|
201
|
+
// outside. It has minted an output against notes it never saw.
|
|
202
|
+
acceptsOversizedMerge: false,
|
|
190
203
|
// Add the optional bound LUD-21 receipt to an honestly bound quote and
|
|
191
204
|
// its verify response. Requires mintToHash, verify and signatures; off
|
|
192
205
|
// by default so the baseline mock remains the current LUD-25 wire.
|
|
@@ -199,10 +212,10 @@ const DEFAULTS = {
|
|
|
199
212
|
// already names a note, an invoice or another quote's output, so two
|
|
200
213
|
// payers' money points at one id
|
|
201
214
|
mintToHashAcceptsUsedH: false,
|
|
202
|
-
// non-compliant, mintToHash on:
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
215
|
+
// non-compliant, mintToHash on: accept h and mandatory comment even when
|
|
216
|
+
// they name different outputs. The comment still binds the note, but the
|
|
217
|
+
// extension claim is false and a receipt-watching signer follows the
|
|
218
|
+
// other commitment.
|
|
206
219
|
mintToHashIgnoresH: false,
|
|
207
220
|
// Which of the three the mock actually says it in. Undefined means all
|
|
208
221
|
// three, which is what an honest mint publishes. Narrowing it changes
|
|
@@ -211,7 +224,32 @@ const DEFAULTS = {
|
|
|
211
224
|
// 'payRequest,mintAddress' is the one that binds without confirming at
|
|
212
225
|
// the moment money moves. Read only when mintToHash is on. Accepts an
|
|
213
226
|
// array or a comma-separated string, so the CLI can pass one.
|
|
214
|
-
mintToHashAdvertisedOn: undefined
|
|
227
|
+
mintToHashAdvertisedOn: undefined,
|
|
228
|
+
|
|
229
|
+
// The mandatory minting commitment in the spelling LUD-25 specifies:
|
|
230
|
+
// `comment = hex(sha256(secret))` (LUD-12), advertised as a
|
|
231
|
+
// `commentAllowed` of at least 64. A number advertises that many
|
|
232
|
+
// characters and reads `comment` as the output name. The conforming
|
|
233
|
+
// default is 64. Setting it false or below 64 deliberately models a
|
|
234
|
+
// SERVICE that cannot mint under the current draft.
|
|
235
|
+
//
|
|
236
|
+
// `h` remains an additive Moneyer compatibility field. When supplied it
|
|
237
|
+
// must be well formed and equal the mandatory comment; it never replaces
|
|
238
|
+
// the comment and cannot make an otherwise unnamed quote valid.
|
|
239
|
+
commentAllowed: 64,
|
|
240
|
+
// non-compliant, commentAllowed on: fall back to a preimage-keyed note
|
|
241
|
+
// when the comment is missing or malformed, instead of refusing. This was
|
|
242
|
+
// the draft's line 80 behaviour and is now the defect - see
|
|
243
|
+
// docs/COMMENT-IS-MANDATORY.md.
|
|
244
|
+
commentFallsBack: false,
|
|
245
|
+
// non-compliant, and narrower: refuse a malformed comment - an empty value
|
|
246
|
+
// included - but fall back when the key is absent entirely. That is the
|
|
247
|
+
// distinction the malformed loop cannot reach, and the commonest way to
|
|
248
|
+
// arrive unnamed, since it is what every ordinary LUD-06 wallet sends.
|
|
249
|
+
commentFallsBackWhenAbsent: false,
|
|
250
|
+
// non-compliant, commentAllowed on: serve LUD-21 verify even on the
|
|
251
|
+
// no-comment fallback, where the preimage it hands out IS the note
|
|
252
|
+
verifyOnUnnamedMint: false
|
|
215
253
|
}
|
|
216
254
|
|
|
217
255
|
export const createMockMint = async (options = {}) => {
|
|
@@ -378,10 +416,9 @@ export const createMockMint = async (options = {}) => {
|
|
|
378
416
|
const invoice = hash ? invoices.get(hash) : null
|
|
379
417
|
if (!invoice) return fail('unknown payment hash')
|
|
380
418
|
invoice.settled = true
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
// then the note is credited there and the preimage is nobody's key.
|
|
419
|
+
// Paying a mint invoice brings its comment-bound note into existence.
|
|
420
|
+
// The payment-hash target is reachable only under deliberate legacy
|
|
421
|
+
// defect flags that allowed an unnamed quote.
|
|
385
422
|
const target = invoice.boundTo ?? hash
|
|
386
423
|
if (!notes.has(target)) mintNote(target, invoice.amountMsat)
|
|
387
424
|
if (invoice.boundTo) boundOutputs.delete(invoice.boundTo)
|
|
@@ -438,7 +475,13 @@ export const createMockMint = async (options = {}) => {
|
|
|
438
475
|
const origin = `http://${req.headers.host}`
|
|
439
476
|
|
|
440
477
|
// ---- LUD-16 payRequest (minting) ----
|
|
441
|
-
|
|
478
|
+
// Both shapes are in the wild. A Lightning Address mint lives under
|
|
479
|
+
// /.well-known/lnurlp/<name>; an LNbits extension serves the same
|
|
480
|
+
// payRequest at a plain path with no well-known anywhere, which is the
|
|
481
|
+
// case a grader must not mistake for a missing mint address document.
|
|
482
|
+
const lnurlpMatch =
|
|
483
|
+
url.pathname.match(/^\/\.well-known\/lnurlp\/(.+)$/) ??
|
|
484
|
+
url.pathname.match(/^\/lnurlp\/(.+)$/)
|
|
442
485
|
if (lnurlpMatch) {
|
|
443
486
|
const user = lnurlpMatch[1]
|
|
444
487
|
if (user !== opts.username && user !== '_') {
|
|
@@ -466,6 +509,11 @@ export const createMockMint = async (options = {}) => {
|
|
|
466
509
|
// the option is on, so a mock started with no options answers
|
|
467
510
|
// exactly what it always answered.
|
|
468
511
|
...(mintToHashPlaces.has('payRequest') ? {mintToHash: true} : {}),
|
|
512
|
+
// LUD-25: "A mint payLink intending to support this SHOULD
|
|
513
|
+
// advertise a commentAllowed of at least 64". A payRequest field
|
|
514
|
+
// only - the mint address document is a withdrawRequest, where a
|
|
515
|
+
// LUD-12 comment has nowhere to go.
|
|
516
|
+
...(opts.commentAllowed ? {commentAllowed: opts.commentAllowed} : {}),
|
|
469
517
|
// A receipt verifier needs the signing key before payment. The
|
|
470
518
|
// baseline mock remains byte-for-byte unchanged when receipts are
|
|
471
519
|
// off; a real node-key signer can alternatively be recovered from
|
|
@@ -599,6 +647,7 @@ export const createMockMint = async (options = {}) => {
|
|
|
599
647
|
// before the option existed.
|
|
600
648
|
let boundTo = null
|
|
601
649
|
let echoBound = false
|
|
650
|
+
let requestedH = null
|
|
602
651
|
if (opts.mintToHash) {
|
|
603
652
|
// absent is not the same as empty: a wallet that sent `h=` meant
|
|
604
653
|
// to bind, and must not be handed an unbound quote in silence
|
|
@@ -627,6 +676,7 @@ export const createMockMint = async (options = {}) => {
|
|
|
627
676
|
return fail('Invalid or already spent k1.')
|
|
628
677
|
}
|
|
629
678
|
if (wellFormed) {
|
|
679
|
+
requestedH = h
|
|
630
680
|
// The echo says "this quote is bound", so an honest mint
|
|
631
681
|
// sets it exactly when it did bind. mintToHashIgnoresH is the
|
|
632
682
|
// mint that says it and does not; leaving 'quote' out of
|
|
@@ -637,6 +687,51 @@ export const createMockMint = async (options = {}) => {
|
|
|
637
687
|
}
|
|
638
688
|
}
|
|
639
689
|
|
|
690
|
+
// The LUD-25 spelling. It is mandatory for every mint quote. The
|
|
691
|
+
// additive `h` spelling may corroborate it, but never substitutes for
|
|
692
|
+
// it and must name the same output when both are present.
|
|
693
|
+
let namedByComment = false
|
|
694
|
+
// >= 64, the same threshold the runner reads the capability at: a
|
|
695
|
+
// commentAllowed too short to carry a 32-byte hash is not this
|
|
696
|
+
// capability, so nothing about it may be required either.
|
|
697
|
+
if (opts.commentAllowed >= 64) {
|
|
698
|
+
const sent = q.get('comment')
|
|
699
|
+
const wellFormed = sent !== null && sent !== '' && /^[0-9a-f]{64}$/i.test(sent)
|
|
700
|
+
{
|
|
701
|
+
if (wellFormed) {
|
|
702
|
+
const h = sent.toLowerCase()
|
|
703
|
+
// Same collision rule the `h` spelling gets: an id already
|
|
704
|
+
// spoken for must never be minted over, whichever parameter
|
|
705
|
+
// named it.
|
|
706
|
+
if (outputIdInUse(h) && !opts.mintToHashAcceptsUsedH) {
|
|
707
|
+
return fail('Invalid or already spent k1.')
|
|
708
|
+
}
|
|
709
|
+
if (
|
|
710
|
+
requestedH !== null &&
|
|
711
|
+
requestedH !== h &&
|
|
712
|
+
!opts.mintToHashIgnoresH
|
|
713
|
+
) {
|
|
714
|
+
return fail('h and comment must name the same output.')
|
|
715
|
+
}
|
|
716
|
+
// The normative comment always binds the quote. The defect flag
|
|
717
|
+
// only models an extension implementation that ignores or fails
|
|
718
|
+
// to compare h.
|
|
719
|
+
boundTo = h
|
|
720
|
+
namedByComment = true
|
|
721
|
+
} else if (
|
|
722
|
+
!opts.commentFallsBack &&
|
|
723
|
+
!(opts.commentFallsBackWhenAbsent && sent === null)
|
|
724
|
+
) {
|
|
725
|
+
// A mint quote without the mandatory comment can only fall back
|
|
726
|
+
// to the payment preimage. The current draft forbids that even
|
|
727
|
+
// when the additive `h` field happened to name an output too.
|
|
728
|
+
return fail(
|
|
729
|
+
'Missing or malformed comment: a hex-encoded 32-byte hashed secret is required to mint.'
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
640
735
|
const preimage = bytesToHex(randomBytes(32))
|
|
641
736
|
const paymentHash = noteId(preimage)
|
|
642
737
|
const pr = fakeInvoice(amount, preimage)
|
|
@@ -647,7 +742,13 @@ export const createMockMint = async (options = {}) => {
|
|
|
647
742
|
}
|
|
648
743
|
invoices.set(paymentHash, invoice)
|
|
649
744
|
const body = {pr, disposable: false}
|
|
650
|
-
|
|
745
|
+
// Verify is safe for every conforming quote because comment-bound
|
|
746
|
+
// minting makes the payment preimage ordinary settlement proof. The
|
|
747
|
+
// conditional remains only so the mock can reproduce the retired
|
|
748
|
+
// preimage-backed behaviour when commentAllowed is deliberately off.
|
|
749
|
+
const verifySafe =
|
|
750
|
+
!(opts.commentAllowed >= 64) || Boolean(boundTo) || opts.verifyOnUnnamedMint
|
|
751
|
+
if (opts.verify && verifySafe) body.verify = `${origin}/verify/${paymentHash}`
|
|
651
752
|
// Appended last, and only when the quote really was bound, so a
|
|
652
753
|
// mock that was never told about any of this answers byte for byte
|
|
653
754
|
// what it always answered.
|
|
@@ -676,11 +777,9 @@ export const createMockMint = async (options = {}) => {
|
|
|
676
777
|
const body = {
|
|
677
778
|
status: 'OK',
|
|
678
779
|
settled: invoice.settled,
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
// bound with its own h: that note is credited elsewhere, so the
|
|
683
|
-
// preimage is an ordinary payment proof and leaks nothing.
|
|
780
|
+
// For a conforming comment-bound quote this is ordinary payment
|
|
781
|
+
// proof and never the bearer secret. It becomes a bearer secret only
|
|
782
|
+
// in the mock's deliberately non-compliant legacy mode.
|
|
684
783
|
preimage: invoice.settled || opts.verifyLeaksEarly ? invoice.preimage : null,
|
|
685
784
|
pr: invoice.pr ?? fakeInvoice(invoice.amountMsat, invoice.preimage)
|
|
686
785
|
}
|
|
@@ -699,6 +798,31 @@ export const createMockMint = async (options = {}) => {
|
|
|
699
798
|
// ---- LUD-03 informational GET ----
|
|
700
799
|
if (url.pathname === '/w') {
|
|
701
800
|
const k1 = q.get('k1')?.toLowerCase()
|
|
801
|
+
// The hash lookup. Notes are already keyed by sha256(k1) internally,
|
|
802
|
+
// so this is a second way in to a lookup the mint can always do - and
|
|
803
|
+
// `h` is only ever read here, never at the callback, where the same
|
|
804
|
+
// letter means the hash of a NEW note.
|
|
805
|
+
const asked = q.get('h')?.toLowerCase()
|
|
806
|
+
if (!k1 && asked && opts.hashLookup) {
|
|
807
|
+
if (!/^[0-9a-f]{64}$/.test(asked)) return fail('Unknown note.')
|
|
808
|
+
const held = notes.get(asked)
|
|
809
|
+
const invent = opts.hashLookup === 'answersUnknown' && !held
|
|
810
|
+
if (!invent) {
|
|
811
|
+
if (!held) return fail('Unknown note.')
|
|
812
|
+
if (held.state === 'burned') return fail('Note already spent.')
|
|
813
|
+
}
|
|
814
|
+
return send({
|
|
815
|
+
tag: 'withdrawRequest',
|
|
816
|
+
callback: `${origin}/w/cb`,
|
|
817
|
+
// k1 is omitted: a wallet asking by hash already holds the secret,
|
|
818
|
+
// which is the only way it could have computed the hash to send.
|
|
819
|
+
...(opts.hashLookup === 'echoesK1' ? {k1: 'c'.repeat(64)} : {}),
|
|
820
|
+
minWithdrawable: 0,
|
|
821
|
+
maxWithdrawable: (held?.amountMsat ?? 21000) + opts.lieAboutValue,
|
|
822
|
+
defaultDescription: 'an LNURLcash note',
|
|
823
|
+
mintPubkey: pubkey
|
|
824
|
+
})
|
|
825
|
+
}
|
|
702
826
|
if (!k1) return fail('Unknown note.')
|
|
703
827
|
if (!/^[0-9a-f]{64}$/.test(k1)) return fail('Unknown note.')
|
|
704
828
|
const note = notes.get(noteId(k1))
|
|
@@ -737,6 +861,8 @@ export const createMockMint = async (options = {}) => {
|
|
|
737
861
|
const h2 = q.get('h2')
|
|
738
862
|
|
|
739
863
|
if (k1s.length === 0) return fail('Missing k1.')
|
|
864
|
+
if (opts.acceptsOversizedMerge && k1s.length > 20) return send({status: 'OK'})
|
|
865
|
+
if (opts.mergeCap > 0 && k1s.length > opts.mergeCap) return fail('too many k1')
|
|
740
866
|
// a repeated k1 would count one note's value twice into the output -
|
|
741
867
|
// refused atomically, as the reference mint does
|
|
742
868
|
if (new Set(k1s).size !== k1s.length) return fail('Invalid or already spent k1.')
|
package/package.json
CHANGED