lnurlcash-conformance 0.8.0 → 0.10.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 CHANGED
@@ -4,6 +4,60 @@ 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
+ ## 0.10.0 - 2026-09-11
8
+
9
+ **Signatures belong to cp1 notes.** The Part 2 rewrite of LUD-25 signs a
10
+ `cp1` note only: a plain hash has nothing to attest to without disclosing
11
+ the secret, so a plain note is unsigned by design. The grader follows it.
12
+
13
+ - `signs the notes it issues` is gone. In its place, `a plain note carries
14
+ no signature, or one that verifies`: a hash rotate answered with a bare
15
+ `{"status":"OK"}` passes, and a mint still issuing the old Part 1
16
+ signature over the hash passes as long as the signature is a true one.
17
+ `mintPubkey` is no longer demanded of a mint that issues no signature.
18
+ - New, last in the note run: `certifies a cp1 note it issues (Part 2)`.
19
+ The grader rotates the note into a fresh `cp1` key, requires a `cs1` in
20
+ `sig` that recovers to `mintPubkey` (or a published previous key) over
21
+ the key and amount, requires the same certificate again on the
22
+ informational GET by `ck1`, then rotates home to a plain secret by that
23
+ `ck1`. A mint that refuses the `cp1` output warns, never fails: Part 2 is
24
+ optional. The mock mint has no Part 2 yet, so it warns here.
25
+ - `vectors/responses.json` follows: a bare `{"status":"OK"}` to a hash
26
+ output is now `ok` (it was `unverifiable`), and so is a split that signs
27
+ only its first hash output. Three cases are new, and carry `output` or
28
+ `change: "cp1"` to say which kind of note the call mints: a `cp1` output
29
+ confirmed without a certificate is `unverifiable`, one certified with a
30
+ `cs1` is `ok`, and a `cp1` change left uncertified is `unverifiable`. A
31
+ consumer driving these cases picks the output by that field; a hash
32
+ where it is absent. Other vector prose that called the hash-note
33
+ signature mandatory now says which output is owed one.
34
+ - CI runs on Node 24.
35
+
36
+ The withdraw-info vector still rejects a `withdrawRequest` with no
37
+ `mintPubkey`; relaxing that for Part 1-only mints is a wallet-side change
38
+ across every kit and is not in this release.
39
+
40
+ ## 0.9.0 - 2026-09-11
41
+
42
+ **Part 2 vectors.** `vectors/part2.json` covers LUD-25 Part 2, notes keyed by
43
+ a public key and spent by a recoverable signature: the four bech32m strings
44
+ (`cp1`, `ck1`, `cs1`, `cx1`, strict per BIP-350), the per-note key tweak
45
+ (watch-only and held), ownership signatures and mint certificates, over 8
46
+ branches (two mnemonics, four hosts including one with a port, both branch
47
+ parities) and 7 indices up to `u32::MAX`. The generator builds them from the
48
+ primitives like every other file here, and they match, byte for byte,
49
+ vectors generated from lnurl-wallet's `src/lib` and confirmed by lnurl-mint.
50
+ The address branch is the reference wallet's `m/139'/1'/d1..d4`, not the
51
+ draft text's `m/139'/d1..d4`; `conventions` records both. The self-check
52
+ recomputes every value, including recovering each `ck1` to its key and each
53
+ `cs1` to the mint's.
54
+
55
+ `vectors/nostr-seed.json` is an extension, not LUD-25, marked as such: a Part
56
+ 2 address branch rooted in a Nostr identity key (`HMAC-SHA256(key,
57
+ "LNURLcash/nostr-seed")`, then the same path), for a holder with no BIP39
58
+ words. heartwood-esp32 derives it on the device and lnurlcash-kit exports
59
+ it; the generated file is identical to the one both already grade against.
60
+
7
61
  ## 0.8.0 - 2026-09-09
8
62
 
9
63
  **A spent note must say so.** LUD-25 [#307][307] settled the hash lookup's
package/README.md CHANGED
@@ -47,6 +47,8 @@ for (const c of cases) {
47
47
  | --- | --- |
48
48
  | `signature.json` | offline verification, both recovery-id orderings, malformed input |
49
49
  | `derivation.json` | deterministic note secrets from a BIP39 seed |
50
+ | `cash-derivation.json` | LUD-25's seed-recoverable note secrets under `m/139'`, with BIP-32's own vector 1 |
51
+ | `part2.json` | Part 2: `cp1`/`ck1`/`cs1`/`cx1`, the per-note key tweak, ownership signatures and mint certificates, on the reference wallet's `m/139'/1'` address path |
50
52
  | `bech32.json` | LUD-01 encoding, round trips, corrupted checksums |
51
53
  | `url-admission.json` | which URLs may be fetched, and why `data:` must never be |
52
54
  | `input-resolution.json` | bech32, LUD-17, Lightning Addresses, bare domains |
@@ -61,6 +63,7 @@ for (const c of cases) {
61
63
  | `settle-for-value.json` | the decision table a server works through to take a note as payment |
62
64
  | `retried-mutation.json` | what makes a repeated mutation a retry rather than a double-spend |
63
65
  | `mint-to-hash.json` | additive `mintToHash` compatibility and optional bound LUD-21 receipts; not baseline LUD-25 |
66
+ | `nostr-seed.json` | a Part 2 address branch rooted in a Nostr identity key, for a holder with no BIP39 words (heartwood-esp32, lnurlcash-kit); an extension, not LUD-25 |
64
67
  | `lifecycle.json` | behavioural requirements, as scenarios to drive |
65
68
  | `threat-suite.json` | the transport/exposure scorecard — candidate spec options against fixed attacks (non-normative) |
66
69
 
@@ -92,7 +95,7 @@ must survive:
92
95
  | `--echoWrongK1` | answers the informational GET with a different `k1` |
93
96
  | `--lieAboutValue=N` | reports a `maxWithdrawable` it never signed |
94
97
  | `--signatureLayout=leading` | emits the recovery id at the other end |
95
- | `--signatures=false` | deliberately violates LUD-25 by issuing no signatures |
98
+ | `--signatures=false` | issues no Part 1 signatures. Allowed since the Part 2 rewrite: a plain note is unsigned by design, so the grader passes it |
96
99
  | `--serverGeneratedSecrets` | hands back a secret it generated — the exposure `h` exists to close |
97
100
  | `--meltNeverSettles` | holds every melt in flight, so notes stay `pending` |
98
101
  | `--meltAlwaysFails` | fails every payment, restoring the note |
package/llms.txt CHANGED
@@ -12,6 +12,9 @@ vectors/index.json:
12
12
 
13
13
  signature.json offline verification, both recovery-id orderings
14
14
  derivation.json deterministic note secrets from a BIP39 seed
15
+ cash-derivation.json LUD-25 m/139' note secrets, plus BIP-32 vector 1
16
+ part2.json Part 2: cp1/ck1/cs1/cx1, the note-key tweak, ownership sigs, certificates;
17
+ address path is the reference wallet's m/139'/1'/d1..d4 (not the text's)
15
18
  bech32.json LUD-01 encoding
16
19
  url-admission.json which URLs may be fetched (https, or http to loopback/.onion)
17
20
  input-resolution.json bech32, LUD-17, Lightning Address, bare domain
@@ -26,6 +29,7 @@ payment-request.json lnurlcashreq1: asking another holder for value
26
29
  settle-for-value.json the decision table for taking a note as payment
27
30
  retried-mutation.json what makes a repeat a retry, not a double-spend
28
31
  mint-to-hash.json naming the note you are buying: h on the pay callback
32
+ nostr-seed.json EXTENSION: Part 2 branch from a Nostr key, HMAC-SHA256(key, "LNURLcash/nostr-seed")
29
33
  lifecycle.json behavioural scenarios
30
34
  threat-suite.json transport/exposure scorecard, options vs attacks (non-normative)
31
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-conformance",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Language-neutral conformance vectors, an adversarial mock mint, and a grader for LNURLcash (LUD-25) implementations",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",
package/runner/index.mjs CHANGED
@@ -5,30 +5,24 @@
5
5
  // with the thing it grades would agree with that implementation's mistakes,
6
6
  // which is the one thing it must never do.
7
7
 
8
- import {bech32} from '@scure/base'
8
+ import {bech32, bech32m} from '@scure/base'
9
9
  import {sha256} from '@noble/hashes/sha2.js'
10
- import {secp256k1} from '@noble/curves/secp256k1.js'
10
+ import {schnorr, secp256k1} from '@noble/curves/secp256k1.js'
11
11
  import {bytesToHex, hexToBytes, utf8ToBytes} from '@noble/hashes/utils.js'
12
12
  import {randomBytes} from 'node:crypto'
13
13
 
14
14
  const noteId = k1 => bytesToHex(sha256(hexToBytes(k1)))
15
15
 
16
- const verifySignature = (k1, amountMsat, signatureHex, pubkeyHex) => {
17
- let sig
18
- try {
19
- sig = hexToBytes(signatureHex)
20
- } catch {
21
- return false
22
- }
16
+ // The "Lightning Signed Message" digest every LUD-25 signature is over.
17
+ const signedMessageDigest = message =>
18
+ sha256(sha256(new Uint8Array([...utf8ToBytes('Lightning Signed Message:'), ...utf8ToBytes(message)])))
19
+
20
+ // Does a 65-byte recoverable signature recover to that key? Both byte
21
+ // orderings are tried: the spec wants r || s || recovery-id, a node's
22
+ // signmessage emits the recovery id first, and a mint that forgot to
23
+ // reorder is still signing with its own key.
24
+ const recoversTo = (digest, sig, pubkeyHex) => {
23
25
  if (sig.length !== 65) return false
24
- const digest = sha256(
25
- sha256(
26
- new Uint8Array([
27
- ...utf8ToBytes('Lightning Signed Message:'),
28
- ...utf8ToBytes(`LNURLcash:${amountMsat}:${noteId(k1)}`)
29
- ])
30
- )
31
- )
32
26
  const leading = new Uint8Array([sig[64], ...sig.subarray(0, 64)])
33
27
  for (const candidate of [leading, sig]) {
34
28
  try {
@@ -43,6 +37,45 @@ const verifySignature = (k1, amountMsat, signatureHex, pubkeyHex) => {
43
37
  return false
44
38
  }
45
39
 
40
+ // A Part 1 signature, over the note's hash. The spec no longer asks for
41
+ // one, but a mint that still issues them must at least issue true ones.
42
+ const verifySignature = (k1, amountMsat, signatureHex, pubkeyHex) => {
43
+ let sig
44
+ try {
45
+ sig = hexToBytes(signatureHex)
46
+ } catch {
47
+ return false
48
+ }
49
+ return recoversTo(signedMessageDigest(`LNURLcash:${amountMsat}:${noteId(k1)}`), sig, pubkeyHex)
50
+ }
51
+
52
+ // A Part 2 certificate: cs1 over the note's raw x-only public key.
53
+ const verifyCertificate = (pubkeyHex32, amountMsat, sig, mintPubkeyHex) =>
54
+ recoversTo(signedMessageDigest(`LNURLcash:${amountMsat}:${pubkeyHex32}`), sig, mintPubkeyHex)
55
+
56
+ // Part 2's bech32m strings: cp1 (32-byte key), ck1 and cs1 (65-byte
57
+ // signature). Longer than BIP-173's 90 characters by design.
58
+ const BECH32M_LIMIT = 200
59
+ const encodeCash = (hrp, bytes) => bech32m.encode(hrp, bech32m.toWords(bytes), BECH32M_LIMIT)
60
+ const decodeCash = (hrp, value, length) => {
61
+ if (typeof value !== 'string') return null
62
+ try {
63
+ const {prefix, words} = bech32m.decode(value, BECH32M_LIMIT)
64
+ if (prefix !== hrp) return null
65
+ const bytes = bech32m.fromWords(words)
66
+ return bytes.length === length ? bytes : null
67
+ } catch {
68
+ return null
69
+ }
70
+ }
71
+
72
+ // The bearer secret of a cp1 note: the key's signature over the fixed
73
+ // message "LNURLcash", r || s || recovery-id, the same value every time.
74
+ const ownershipProof = secretKey => {
75
+ const lead = secp256k1.sign(signedMessageDigest('LNURLcash'), secretKey, {format: 'recovered', prehash: false})
76
+ return encodeCash('ck', new Uint8Array([...lead.subarray(1), lead[0]]))
77
+ }
78
+
46
79
  // LUD-17: lnurlw://host/path is https://host/path, or http:// when the host
47
80
  // is an onion service (the spec) or loopback (development). A plain
48
81
  // https:// or http:// URL passes through untouched, so a caller can hand
@@ -1046,28 +1079,33 @@ export const gradeNote = async (noteUrl, report, options = {}) => {
1046
1079
  return 'burned the old secret, minted the new'
1047
1080
  })
1048
1081
 
1049
- await report.check('signs the notes it issues', async () => {
1050
- assert(info.mintPubkey, 'no mintPubkey advertised - offline verification is mandatory')
1051
- assert(isCompressedPubkey(info.mintPubkey), 'mintPubkey is not a 33-byte compressed secp256k1 key')
1052
- assert(currentSig, 'the rotate returned no sig - offline verification is mandatory')
1053
- // A mint that has rotated its signing key may publish the old ones as
1054
- // previousPubkeys, so notes it issued before the rotation still
1055
- // verify. Any key it currently stands behind is an acceptable signer
1056
- // for grading purposes. That is a narrower claim than it looks: it
1057
- // says the signature is genuine, not that a wallet should accept the
1058
- // new key - LUD-25 puts that decision with the holder.
1059
- const signedBy = [info.mintPubkey, ...previousPubkeys].find(key =>
1060
- verifySignature(current, info.maxWithdrawable, currentSig, key)
1061
- )
1062
- assert(
1063
- signedBy,
1064
- previousPubkeys.length > 0
1065
- ? 'the signature verifies against neither the advertised mintPubkey nor any published previous key'
1066
- : 'the signature does not verify against the advertised mintPubkey and amount'
1067
- )
1068
- return signedBy === info.mintPubkey
1082
+ // A mint that has rotated its signing key may publish the old ones as
1083
+ // previousPubkeys, so notes it issued before the rotation still verify.
1084
+ // Any key it currently stands behind is an acceptable signer for grading
1085
+ // purposes. That is a narrower claim than it looks: it says the
1086
+ // signature is genuine, not that a wallet should accept the new key -
1087
+ // LUD-25 puts that decision with the holder.
1088
+ const signerOf = verifies => [info.mintPubkey, ...previousPubkeys].find(key => verifies(key))
1089
+ const describeSigner = signedBy =>
1090
+ signedBy === info.mintPubkey
1069
1091
  ? 'verified offline'
1070
1092
  : `verified offline against a previous signing key (${signedBy.slice(0, 16)}...)`
1093
+ const unverified = () =>
1094
+ previousPubkeys.length > 0
1095
+ ? 'the signature verifies against neither the advertised mintPubkey nor any published previous key'
1096
+ : 'the signature does not verify against the advertised mintPubkey and amount'
1097
+
1098
+ await report.check('a plain note carries no signature, or one that verifies', async () => {
1099
+ // LUD-25 Part 2 signs cp1 notes only: a hash has nothing to attest to
1100
+ // without disclosing the secret, so a plain note is unsigned by
1101
+ // design. A mint that still issues the old Part 1 signature over the
1102
+ // hash is harmless, as long as the signature is a true one.
1103
+ if (currentSig === null) return 'unsigned, as Part 2 specifies for a plain note'
1104
+ assert(info.mintPubkey, 'a sig with no mintPubkey advertised verifies against nothing')
1105
+ assert(isCompressedPubkey(info.mintPubkey), 'mintPubkey is not a 33-byte compressed secp256k1 key')
1106
+ const signedBy = signerOf(key => verifySignature(current, info.maxWithdrawable, currentSig, key))
1107
+ assert(signedBy, unverified())
1108
+ return `a legacy Part 1 signature, ${describeSigner(signedBy)}`
1071
1109
  })
1072
1110
 
1073
1111
  await report.check('reports a spent hash distinguishably from an unknown hash', async () => {
@@ -1095,12 +1133,10 @@ export const gradeNote = async (noteUrl, report, options = {}) => {
1095
1133
  })
1096
1134
 
1097
1135
  await report.check('keeps signatures off the informational endpoint', async () => {
1098
- // LUD-25: "Signatures are only ever delivered in the
1099
- // withdrawSuccessResponse of a rotate, split or merge, the
1100
- // informational endpoint never returns one." A mint that hands one out
1101
- // here lets anyone holding only a note's PUBLIC url mint a certificate
1102
- // for it, and invites a wallet to treat the informational answer as an
1103
- // offline proof when it is an online one.
1136
+ // A plain note has no certificate, so its informational GET carries no
1137
+ // sig. Part 2 does hand out a cs1 here for a cp1 note, and the Part 2
1138
+ // check below expects it; this probe is by hex k1, where one would
1139
+ // invite a wallet to treat an online answer as an offline proof.
1104
1140
  const here = new URL(url)
1105
1141
  here.searchParams.set('k1', current)
1106
1142
  const body = await get(here)
@@ -1418,6 +1454,61 @@ export const gradeNote = async (noteUrl, report, options = {}) => {
1418
1454
  return body.reason
1419
1455
  })
1420
1456
 
1457
+ // LUD-25 Part 2. The spec's one MUST for signatures lives here: a cp1
1458
+ // note is a public key, and the SERVICE certifies every one it issues
1459
+ // with a cs1 over (key, amount) that recovers to mintPubkey. Part 2 is
1460
+ // optional, so a SERVICE that refuses the cp1 output warns rather than
1461
+ // fails. Last, because the note comes back as a plain secret only if
1462
+ // the rotate home succeeds.
1463
+ await report.check('certifies a cp1 note it issues (Part 2)', async () => {
1464
+ const secretKey = secp256k1.utils.randomSecretKey()
1465
+ const pubkey = schnorr.getPublicKey(secretKey)
1466
+ const cp1 = encodeCash('cp', pubkey)
1467
+ const out = new URL(info.callback)
1468
+ out.searchParams.append('k1', current)
1469
+ out.searchParams.append('p1', cp1)
1470
+ const body = await get(out)
1471
+ if (body.status === 'ERROR') throw soft(`Part 2 not offered: a cp1 output was refused (${body.reason})`)
1472
+ // The plain secret is burned either way; from here the bearer secret
1473
+ // is the key's ownership proof, until the rotate home below.
1474
+ const ck1 = ownershipProof(secretKey)
1475
+ current = ck1
1476
+ assert(info.mintPubkey, 'issued a cp1 note with no mintPubkey advertised - nothing to verify its certificate against')
1477
+ assert(isCompressedPubkey(info.mintPubkey), 'mintPubkey is not a 33-byte compressed secp256k1 key')
1478
+ const pubkeyHex = bytesToHex(pubkey)
1479
+ const certificate = decodeCash('cs', body.sig, 65)
1480
+ assert(certificate, `the rotate to a cp1 output returned no cs1 certificate in sig (got ${JSON.stringify(body.sig)})`)
1481
+ const signedBy = signerOf(key => verifyCertificate(pubkeyHex, info.maxWithdrawable, certificate, key))
1482
+ assert(signedBy, unverified())
1483
+
1484
+ // The informational GET by ck1 delivers the certificate again, so a
1485
+ // holder need not rotate just to obtain one.
1486
+ const byKey = new URL(url)
1487
+ byKey.searchParams.set('k1', ck1)
1488
+ const lookup = await get(byKey)
1489
+ assert(lookup.status !== 'ERROR', `the cp1 note is not spendable by its ck1: ${lookup.reason}`)
1490
+ assert(
1491
+ lookup.maxWithdrawable === info.maxWithdrawable,
1492
+ `value changed across a rotate to cp1: ${info.maxWithdrawable} -> ${lookup.maxWithdrawable}`
1493
+ )
1494
+ const again = decodeCash('cs', lookup.sig, 65)
1495
+ assert(again, 'the informational GET by ck1 returned no cs1 certificate')
1496
+ assert(
1497
+ signerOf(key => verifyCertificate(pubkeyHex, info.maxWithdrawable, again, key)),
1498
+ 'the certificate on the informational GET does not verify'
1499
+ )
1500
+
1501
+ // Home: back to a plain secret, spent by the ck1.
1502
+ const fresh = bytesToHex(randomBytes(32))
1503
+ const home = new URL(info.callback)
1504
+ home.searchParams.append('k1', ck1)
1505
+ home.searchParams.append('p1', noteId(fresh))
1506
+ const back = await get(home)
1507
+ assert(back.status === 'OK', `the cp1 note could not be rotated back to a plain secret by its ck1: ${back.reason}`)
1508
+ current = fresh
1509
+ return `${describeSigner(signedBy)}; the plain note it rotated home to is ${back.sig === undefined ? 'unsigned' : 'still signed the Part 1 way'}`
1510
+ })
1511
+
1421
1512
  return {finalSecret: current, noteUrl: (() => {
1422
1513
  const u = new URL(url)
1423
1514
  u.searchParams.set('k1', current)
@@ -22,6 +22,8 @@
22
22
  "payment-request.json",
23
23
  "settle-for-value.json",
24
24
  "retried-mutation.json",
25
- "mint-to-hash.json"
25
+ "mint-to-hash.json",
26
+ "part2.json",
27
+ "nostr-seed.json"
26
28
  ]
27
29
  }
@@ -58,7 +58,7 @@
58
58
  "the connection drops after the SERVICE applied it",
59
59
  "the HTTP stack silently resends the identical request"
60
60
  ],
61
- "requirement": "a SERVICE MUST recognize the byte-identical retry from the same k1 set, h, h2 and amount, and return the original success with the same sig and sig2 without moving balance again. A WALLET still persists every fresh output secret before sending the first request and keeps it across an ambiguous transport failure. A request that changes any recorded field is a genuine double-spend attempt and gets the ordinary already-spent refusal."
61
+ "requirement": "a SERVICE MUST recognize the byte-identical retry from the same k1 set, h, h2 and amount, and return the original success, with the same sig and sig2 where the outputs had any, without moving balance again. A WALLET still persists every fresh output secret before sending the first request and keeps it across an ambiguous transport failure. A request that changes any recorded field is a genuine double-spend attempt and gets the ordinary already-spent refusal."
62
62
  },
63
63
  {
64
64
  "name": "settle a merge or split output",
@@ -0,0 +1,133 @@
1
+ {
2
+ "version": 1,
3
+ "spec": "LUD-25 draft (lnurl/luds#301)",
4
+ "extension": true,
5
+ "description": "An extension, not LUD-25: a Part 2 address branch rooted in a Nostr identity key, for a holder with no BIP39 words. seed = HMAC-SHA256(key = identity (the 32-byte secret key), msg = utf8(\"LNURLcash/nostr-seed\")); the branch is then the reference wallet's m/139'/1'/d1..d4 from that seed exactly as in part2.json, and every note field means what it means there. identityPubkey is the identity's x-only public key, the npub a lightning address belongs to. heartwood-esp32 derives this on the device and lnurlcash-kit exports it (deriveNostrAddressNode); a mint sees an ordinary cx1.",
6
+ "label": "LNURLcash/nostr-seed",
7
+ "cases": [
8
+ {
9
+ "identity": "0000000000000000000000000000000000000000000000000000000000000001",
10
+ "identityPubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
11
+ "host": "moneyer.dev",
12
+ "seed": "3ae0ea260b8066e219db88cf06b3ea9a170104875f3ea70a8434a2bc478ea121",
13
+ "addressNode": "6ec86f66d87919b32347fdc6b93855bdf3649e2641fc57ee977b72ce9d34625f63246e942e8f68ec0b82c7bcc1b39e5643a2a304356bcb3821163805f9d2d700",
14
+ "cx1": "cx1haqpwf60sc5gkzar0282ma4t56fm9fzlm43nr23ztyvxkl0w8j4xxfrwjshg768vpwpv00xpkw09vsaz5vzr267t8qs3vwq9l8fdwqq605q4l",
15
+ "notes": [
16
+ {
17
+ "index": 0,
18
+ "noteSecretKey": "6257b20051d8bf990abef9274b05eec72deed8f6a6adae958e91ceb51e06136a",
19
+ "notePubkey": "5d4fc85381b2b548c802d96a3bb98c2b278fef566e4888545abf1ebb7f3bfc79",
20
+ "cp1": "cp1t48us5upk2653jqzm94rhwvv9vnclm6kdeygs4z6hu0tkleml3uscuyhw6",
21
+ "ck1": "ck1v5ua6x3ldunqyhj5mh89dyxt3g27uy2n8awq3cktj83jtn7hux0q9dgh4g6mly78vgmh4cgf34dq7l8ww7vadx644scv9h8tekq672qqr60q6e"
22
+ },
23
+ {
24
+ "index": 1,
25
+ "noteSecretKey": "ad44ab63c2ca7890e2a3a7644597dfcb7ad5aece2722d3506aa1b0a2df4d8461",
26
+ "notePubkey": "d0ca20135641a5e43124476048992fc61dce1fba6e787d47d34e5a32de86675c",
27
+ "cp1": "cp16r9zqy6kgxj7gvfygasy3xf0ccwuu8a6deu8637nfedr9h5xvawqrq3sk4",
28
+ "ck1": "ck1p99ktdrr2vpf8unme3mjd9wvgx22840n05aqh78vw9kma337xntshw7pdldn4dujn3e23hgyx9znr3e60g8293gkv0h0xk9mskgqd9qq0cjpsp"
29
+ },
30
+ {
31
+ "index": 7,
32
+ "noteSecretKey": "e9acf5438666e2563e12f77f904298a1c11b6f580ae44adfbaad5897b5f85893",
33
+ "notePubkey": "f2bbf60e6d9697e488cdbfe4449045541e753e3062f14d06ad0934c9e98ebcfe",
34
+ "cp1": "cp172alvrndj6t7fzxdhljyfyz92s08203svtc56p4dpy6vn6vwhnlqv0yvd2",
35
+ "ck1": "ck1r55fx609dq0pz8qg6q73hsa0v4wkdz6ldxqr2rshg72ydvk3aezrv2c77xw8smqckgjrj9h5vp5n4tp04v6hae0yazrzcfcj4p7saespmxl28s"
36
+ }
37
+ ]
38
+ },
39
+ {
40
+ "identity": "0000000000000000000000000000000000000000000000000000000000000001",
41
+ "identityPubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
42
+ "host": "mint.example",
43
+ "seed": "3ae0ea260b8066e219db88cf06b3ea9a170104875f3ea70a8434a2bc478ea121",
44
+ "addressNode": "06825d4c6b2d170fa57fa0449dfd5c8576eb166f3c35a8e39cf14db396e2ec9c7c338e8dca8b99b65031b0fbc5740fae7328bb55ff156791f5cd9dc393b4da30",
45
+ "cx1": "cx14tvrmc8yw86su4puf327tv28g5hl2re7p2ygwyttfcsqqg6hn9q8cvuw3h9ghxdk2qcmp779ws86uueghd2l79t8j86um8wrjw6d5vqfhgw4f",
46
+ "notes": [
47
+ {
48
+ "index": 0,
49
+ "noteSecretKey": "3fe7bd30638deca734f56bec3adc015b32b5095ccd373113aae9392ef13aad09",
50
+ "notePubkey": "5d8b9c7229178df9413e818e50535b7c53e8cb9ce519de831d0fd6548beab22e",
51
+ "cp1": "cp1tk9ecu3fz7xljsf7sx89q56m03f73juuu5vaaqcaplt9fzl2kghqsdyjd4",
52
+ "ck1": "ck154300fcxpmnr9308fs7ut66snvc2xhez2uap2ngn32zha2ew3kv9zz8hjnzncewzdrpk8jxgheg4wthclvl4cdnygkpn9c9kda64swgq34camt"
53
+ },
54
+ {
55
+ "index": 1,
56
+ "noteSecretKey": "a76597b438f20a6c57995d615ad8fa8f63dadbf2efbcdc8a73e4bdf9c605b203",
57
+ "notePubkey": "66866a6420435fea5c9179e5ea9c31fb5cbd985c7f590a2a509cd5ef09e91b5d",
58
+ "cp1": "cp1v6rx5epqgd075hy308j748p3ldwtmxzu0avs52jsnn277z0frdwslr7t47",
59
+ "ck1": "ck1ea8utp0c6l35xgj6huluu6mypcvuqd9le4mmfm6dlcgl20f60lnpvjxnmtx6afpuxpc70ngdt0uhx07kuzws8amg6fcew2fetanrvyqpljdau4"
60
+ },
61
+ {
62
+ "index": 7,
63
+ "noteSecretKey": "8fb88ed836e0fe83728642938d6463cee74781b7d4b0d313717be6de50b6d046",
64
+ "notePubkey": "14c823d28bbdd662c788ab968ac56f6aef6cb88e622afe11bb6ac2134f24fb80",
65
+ "cp1": "cp1znyz855thhtx93ug4wtg43t0dthkewywvg40uydmdtppxneylwqq3nehx2",
66
+ "ck1": "ck1gtw6p9hglznv5xhw0z5elf8u66mc2s38gq7703w9mfgl2pc7ds7q092nl8jz8nqh523qw5640lg8gw8kvj60u97mcu9l5zgfkp9hmnqpstf30d"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "identity": "7f4c11a9742721d66e40e321ca70b682c27f7422190c84a187525e69e6038369",
72
+ "identityPubkey": "99e98a193119f2a8adbe6c7da81d3cd64a25742cc49f51f4ec0da44b22bf09bf",
73
+ "host": "moneyer.dev",
74
+ "seed": "efd666778ca0baac6648f11f4b3c428655a1a380caa89309a4d18ae1529f0999",
75
+ "addressNode": "05d3aa38d42680e7d812aca8f11bedee79b19bcd0ecbe3ecb0fecf3efd5678f9ddcf32d0170b91e809ed08f3a5c06b47042921b7a230072d772edcf58fcb2b89",
76
+ "cx1": "cx168cuwapgp0hd6h7resq0uh78eze300vg7ef6t4kmywpcmaptdemdmnej6qtshy0gp8ks3ua9cp45wppfyxm6yvq894mjah843l9jhzg4rxu74",
77
+ "notes": [
78
+ {
79
+ "index": 0,
80
+ "noteSecretKey": "efe31724912e78b6a9262deecc2ad25c70022a551ca7dca2a99058fab81f00cf",
81
+ "notePubkey": "5fcdc98dad667ff47d8e0394d9c4fb5664269558de303a4704281c64b01f4ca3",
82
+ "cp1": "cp1tlxunrddvellglvwqw2dn38m2ejzd92cmccr53cy9qwxfvqlfj3s74l9ar",
83
+ "ck1": "ck1yvcxftc6l8pduzetwhxzxmnled7w4xzgkldy7dsxga90rxux8r7kjy5l9wphc0p79k526wg3x4plk0nx54erv3z0gyt4r8082aqypyqpve8twp"
84
+ },
85
+ {
86
+ "index": 1,
87
+ "noteSecretKey": "4d2988c9a2dbec87b1c737fed42e37424a4a7cf72f2b3721a0668f40b4f8ad02",
88
+ "notePubkey": "ae1fc4e14097b00e67343e62c3fad53eb836b201bd3774e24185b137fb7a6c7d",
89
+ "cp1": "cp14c0ufc2qj7cquee58e3v87k486urdvsph5mhfcjpskcn07m6d37s6qvndu",
90
+ "ck1": "ck12ggha0c0za5clvnx0cdqasuwexn947uyu6wmady9wvrt9xuzj4j3nsjk8r0uzq6yzyq6jmtnpeqarwxq28c7cz756g03et2frnqjtyqqp927xj"
91
+ },
92
+ {
93
+ "index": 7,
94
+ "noteSecretKey": "17bfb4cc3c3768c1cce2b33e8b9600e6231bbd925cd3fcc413009915f0348e4d",
95
+ "notePubkey": "254feeb2016cc8deb154983d3da72dcad2c202736eca9031dcefd9693a9faae6",
96
+ "cp1": "cp1y487avspdnydav25nq7nmfedetfvyqnndm9fqvwualvkjw5l4tnqkypely",
97
+ "ck1": "ck1eesa3wacyrffpnl267vns608cah8cr6n35rlhnydwnq9r94jc2xxfceh0pt40f388u4k995m7f6dw5jhzcpgm97pjwf0dk0h7rj8rhcqt54nxa"
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ "identity": "7f4c11a9742721d66e40e321ca70b682c27f7422190c84a187525e69e6038369",
103
+ "identityPubkey": "99e98a193119f2a8adbe6c7da81d3cd64a25742cc49f51f4ec0da44b22bf09bf",
104
+ "host": "127.0.0.1:8899",
105
+ "seed": "efd666778ca0baac6648f11f4b3c428655a1a380caa89309a4d18ae1529f0999",
106
+ "addressNode": "9df193134dacac4561674b0f099f2dfcbfea884f3df655aa53117798cb3e3065a2eb93a6d2be16ca6acee95a43317b870a718bf96b0a2163e1c99c52f96518b7",
107
+ "cx1": "cx10l8vgpvm7nsa6x8pn87ampuxvevw9eyxj2x2dzg72zk7fzrcfg7696un5mftu9k2dt8wjkjrx9acwzn330ukkz3pv0sun8zjl9j33dcymzxac",
108
+ "notes": [
109
+ {
110
+ "index": 0,
111
+ "noteSecretKey": "72242afc3854a1c5d41748b25d2fddc06b75b663b69cd051eb8d102262703696",
112
+ "notePubkey": "c01e87a6ebb3cb6596284a6b0da265df73bdaf885bfd0760929c7e2828cbfd0c",
113
+ "cp1": "cp1cq0g0fhtk09kt93gff4smgn9maemmtugt07swcyjn3lzs2xtl5xqjvp56d",
114
+ "ck1": "ck15kws54u2vk84hzmf2a6wm7yhnzljl907lmnsr5grgeg06evcj464qvccyflnzqdm44aenxg4gl2dmxfy34pvk3unt4k5ptl7yfr3hngqhn3gvc"
115
+ },
116
+ {
117
+ "index": 1,
118
+ "noteSecretKey": "cea7c90475f1cc5c19784bccee03b0f4b3972d6a8c3710fb820720cbd74e0d1d",
119
+ "notePubkey": "ba9bcbb4e1b9e5884b40f5c3b15d59352814ca77bd226d51960f289a22e96319",
120
+ "cp1": "cp1h2duhd8ph8jcsj6q7hpmzh2ex55pfjnhh53x65vkpu5f5ghfvvvsa3zqpx",
121
+ "ck1": "ck126c3ppdcs0708ya2r8ffde5ggtnnp5tdu6und5tnygqu804t034rcu7mgefx0feusscnqr3ym78l2mywev5apjjptg9mm2g9d7sse3gp6tm5ss"
122
+ },
123
+ {
124
+ "index": 7,
125
+ "noteSecretKey": "4ea4a3488ae292b38074ef02996ffe5cc82be76e6fc509b0793d8e22be0c181f",
126
+ "notePubkey": "49ef82ee7810b6d8099635eb422feca997850663daded48788993ec7d9585965",
127
+ "cp1": "cp1f8hc9mnczzmdszvkxh45ytlv4xtc2pnrmt0dfpugnylv0k2ct9jst4p42x",
128
+ "ck1": "ck16esd3xmpy5gw2elw3d83hpye7ygf5mtzcuh8aygwpevc9fjwjdmnwldmj3kajcu9da2kz92n5uty3ryqeckjxe586p2ncrsj20re9mgq7vv73l"
129
+ }
130
+ ]
131
+ }
132
+ ]
133
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
3
  "spec": "LUD-25 draft (lnurl/luds#301)",
4
- "description": "A note is an ordinary LUD-03 withdrawRequest URL whose k1 IS the asset. `amount` alongside it is only a claim by whoever encoded the note - the authoritative value is always maxWithdrawable from an informational GET. A SERVICE returning a rotate, split or merge MUST include the offline-verification signature in `sig` (and `sig2` for the second split output).",
4
+ "description": "A note is an ordinary LUD-03 withdrawRequest URL whose k1 IS the asset. `amount` alongside it is only a claim by whoever encoded the note - the authoritative value is always maxWithdrawable from an informational GET. A SERVICE returning a rotate, split or merge to a cp1 output MUST include its certificate in `sig` (and `sig2` for the second split output); a plain hash output carries none, and a legacy Part 1 signature on one is harmless where it verifies.",
5
5
  "parse": [
6
6
  {
7
7
  "url": "https://mint.example/w?k1=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&amount=21000",