did-beta-bch 2.0.1 → 3.1.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/PROTOCOL.md +116 -154
- package/README.md +83 -30
- package/dist/anchor-client.d.ts +30 -0
- package/dist/anchor-client.js +107 -0
- package/dist/anchor-verification.d.ts +46 -0
- package/dist/anchor-verification.js +282 -0
- package/dist/binding.d.ts +12 -3
- package/dist/binding.js +51 -3
- package/dist/bip47.d.ts +50 -0
- package/dist/bip47.js +171 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -3
- package/dist/verify.d.ts +36 -1
- package/dist/verify.js +138 -7
- package/package.json +3 -3
package/PROTOCOL.md
CHANGED
|
@@ -1,75 +1,55 @@
|
|
|
1
1
|
# Cashlet DID Protocol Specification
|
|
2
2
|
|
|
3
|
-
Version: `
|
|
4
|
-
Status:
|
|
3
|
+
Version: `3.0.0`
|
|
4
|
+
Status: Final
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
signatures. Existing v1 bindings must be re-created and re-published.
|
|
9
|
-
|
|
10
|
-
---
|
|
6
|
+
Cashlet DID v3 is a breaking protocol update. Conforming verifiers MUST reject
|
|
7
|
+
v1 and v2 binding records. Existing identities must be rebound and republished.
|
|
11
8
|
|
|
12
9
|
## 1. Identifiers
|
|
13
10
|
|
|
14
11
|
| Constant | Value |
|
|
15
12
|
|---|---|
|
|
16
13
|
| Protocol ID | `cashlet-did` |
|
|
17
|
-
| Protocol version | `
|
|
18
|
-
| Binding event kind | `30078`
|
|
14
|
+
| Protocol version | `3.0.0` |
|
|
15
|
+
| Binding event kind | `30078` |
|
|
16
|
+
| Rotation authorization kind | `30080` |
|
|
19
17
|
| Credential event kind | `30085` |
|
|
20
18
|
| Revocation event kind | `5` (NIP-09) |
|
|
19
|
+
| Dual-key challenge kind | `27235` (NIP-98 event format) |
|
|
21
20
|
| Nostr derivation path | `m/44'/1237'/<account>'/0/0` (NIP-06) |
|
|
22
21
|
|
|
23
|
-
## 2.
|
|
22
|
+
## 2. Binding and BCH ownership proof
|
|
24
23
|
|
|
25
24
|
A binding record has recursively sorted object keys, no whitespace, and omits
|
|
26
25
|
undefined/absent fields:
|
|
27
26
|
|
|
28
27
|
```json
|
|
29
28
|
{
|
|
30
|
-
"bch": {
|
|
31
|
-
|
|
32
|
-
"network": "mainnet" | "testnet" | "chipnet",
|
|
33
|
-
"paymentCode": "<BIP47 payment code>" // optional
|
|
34
|
-
},
|
|
35
|
-
"createdAt": <unix seconds>,
|
|
29
|
+
"bch": { "address": "<prefixed-cashaddr>", "network": "mainnet|testnet|chipnet" },
|
|
30
|
+
"createdAt": 0,
|
|
36
31
|
"nostr": { "pubkey": "<32-byte lowercase hex>" },
|
|
37
32
|
"nonce": "<64 lowercase hex chars>",
|
|
38
|
-
"previousBindingId": "<64 lowercase hex chars>", // optional
|
|
39
33
|
"protocol": "cashlet-did",
|
|
40
|
-
"purpose": "identity-binding-
|
|
41
|
-
"version": "
|
|
34
|
+
"purpose": "identity-binding-v3",
|
|
35
|
+
"version": "3.0.0"
|
|
42
36
|
}
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`testnet
|
|
48
|
-
|
|
49
|
-
## 3. BCH signing payload and wallet contract
|
|
39
|
+
Optional fields are `bch.paymentCode` and `previousBindingId`. The binding ID
|
|
40
|
+
is lowercase hex `SHA-256(canonicalize(record_without_bindingId))`. Arrays
|
|
41
|
+
preserve order. Mainnet addresses use `bitcoincash:`; testnet and chipnet use
|
|
42
|
+
`bchtest:`.
|
|
50
43
|
|
|
51
|
-
The exact UTF-8 message
|
|
44
|
+
The exact UTF-8 message supplied to the BCH wallet is:
|
|
52
45
|
|
|
53
46
|
```text
|
|
54
|
-
cashlet-did/bch-proof/
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The subset contains exactly these keys:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"bchAddress": "<prefixed-cashaddr>",
|
|
62
|
-
"bchNetwork": "mainnet" | "testnet" | "chipnet",
|
|
63
|
-
"bindingId": "<64 lowercase hex chars>",
|
|
64
|
-
"createdAt": <unix seconds>,
|
|
65
|
-
"nonce": "<64 lowercase hex chars>",
|
|
66
|
-
"nostrPubkey": "<32-byte lowercase hex>",
|
|
67
|
-
"purpose": "identity-binding-v2"
|
|
68
|
-
}
|
|
47
|
+
cashlet-did/bch-proof/v3.0.0\n<canonicalized subset>
|
|
69
48
|
```
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
50
|
+
The subset contains exactly `bindingId`, `bchAddress`, `bchNetwork`,
|
|
51
|
+
`nostrPubkey`, `nonce`, `purpose`, and `createdAt`. Pass this exact text to the
|
|
52
|
+
wallet; do not pre-hash, add a Bitcoin prefix, or otherwise transform it. The
|
|
73
53
|
wallet computes:
|
|
74
54
|
|
|
75
55
|
```text
|
|
@@ -77,168 +57,150 @@ SHA256d(0x18 || "Bitcoin Signed Message:\n" ||
|
|
|
77
57
|
CompactSize(UTF8(message).length) || UTF8(message))
|
|
78
58
|
```
|
|
79
59
|
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
60
|
+
The response MUST be canonical padded Base64 for exactly 65 bytes:
|
|
61
|
+
`[header 31–34] || [32-byte r] || [32-byte s]`. The header selects a compressed
|
|
62
|
+
public key and recovery ID 0–3. Raw hex, 64-byte signatures, uncompressed
|
|
63
|
+
headers, non-canonical Base64, and v1/v2 proof formats are rejected.
|
|
83
64
|
|
|
84
|
-
|
|
65
|
+
## 3. Binding Nostr event
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The header is `27 + 4 + recovery_id`; recovery IDs are `0–3`, and `+4`
|
|
91
|
-
specifies a compressed public key. V2 accepts only this compressed-key form.
|
|
92
|
-
Verifiers decode Base64, subtract 31 from the header, recover the compressed
|
|
93
|
-
public key, and derive its prefixed BCH CashAddr. Raw hex, 64-byte signatures,
|
|
94
|
-
uncompressed headers, non-canonical Base64, and v1 signatures are rejected.
|
|
95
|
-
|
|
96
|
-
### Wallet interoperability evidence
|
|
97
|
-
|
|
98
|
-
Paytaca's WalletConnect implementation signs the supplied message with the
|
|
99
|
-
Bitcoin message prefix, `bigIntToCompactUint`, double-SHA256, and recoverable
|
|
100
|
-
compact ECDSA; it adds `31` for compressed keys and returns Base64:
|
|
101
|
-
[Paytaca source](https://github.com/Paytaca/paytaca-app/blob/41c785b9f48dee337d2da3f33e082e5210d462d0/src/wallet/walletconnect2/index.js).
|
|
102
|
-
The BCH WalletConnect BCR also specifies Electron Cash-compatible signed
|
|
103
|
-
messages: [BCR](https://github.com/mainnet-pat/wc2-bch-bcr/blob/55e3e37142ddc7b30ad63c7330ef8df252fec715/README.md).
|
|
104
|
-
The library keeps this contract transport-neutral; validate each wallet
|
|
105
|
-
release's response in integration tests before advertising support.
|
|
106
|
-
|
|
107
|
-
## 4. Nostr event (kind 30078)
|
|
108
|
-
|
|
109
|
-
The event timestamp MUST equal the binding's `createdAt`. Required tags:
|
|
67
|
+
Publish a kind `30078` event whose timestamp equals `binding.createdAt`.
|
|
68
|
+
Required tags:
|
|
110
69
|
|
|
111
70
|
```json
|
|
112
71
|
[
|
|
113
72
|
["d", "<bindingId>"],
|
|
114
73
|
["bch", "<cashaddr>", "<network>"],
|
|
115
|
-
["protocol", "cashlet-did", "
|
|
74
|
+
["protocol", "cashlet-did", "3.0.0"],
|
|
116
75
|
["t", "cashlet-did"],
|
|
117
76
|
["bch-proof", "bitcoin-message/compact-base64", "<exact message>", "<signature Base64>"]
|
|
118
77
|
]
|
|
119
78
|
```
|
|
120
79
|
|
|
121
|
-
Optional tags
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```
|
|
80
|
+
Optional tags are `bch-payment-code`, `supersedes`, `rotation-proof`, and
|
|
81
|
+
`bch-anchor`. Content is canonical JSON with `protocol`, `version`,
|
|
82
|
+
`bindingId`, `bch`, `nonce`, and `createdAt`, plus optional `previousBindingId`
|
|
83
|
+
and embedded `rotationAuthorization`. The Nostr event signature covers all
|
|
84
|
+
tags and content. A `bch-anchor` tag is an untrusted claim until independently
|
|
85
|
+
verified under §8.
|
|
128
86
|
|
|
129
|
-
|
|
130
|
-
`bindingId`, `bch`, `nonce`, `createdAt`, and optional `previousBindingId`.
|
|
131
|
-
`bch` contains `address`, `network`, and optional `paymentCode`. Nostr event
|
|
132
|
-
signatures protect all event tags and content.
|
|
87
|
+
## 4. Discovery, revocation, and verification
|
|
133
88
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Discover v2 records with this NIP-01 filter:
|
|
89
|
+
Discover v3 bindings with:
|
|
137
90
|
|
|
138
91
|
```json
|
|
139
92
|
{ "kinds": [30078], "authors": ["<pubkey hex>"], "#t": ["cashlet-did"] }
|
|
140
93
|
```
|
|
141
94
|
|
|
142
95
|
Revocations are kind `5` events from the same author with `["e", "<event id>"]`
|
|
143
|
-
tags. Filter revoked records before selecting the newest
|
|
96
|
+
tags. Filter revoked records before selecting the newest binding.
|
|
144
97
|
|
|
145
|
-
|
|
98
|
+
A verifier MUST:
|
|
146
99
|
|
|
147
100
|
1. Verify the NIP-01 event signature and require kind `30078`.
|
|
148
101
|
2. Require exactly one correctly shaped `d`, `bch`, `protocol`, `t`, and
|
|
149
|
-
`bch-proof` tag
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
4. Recompute the binding ID and reconstruct the exact v2 BCH signing payload.
|
|
156
|
-
The proof tag's message must be byte-for-byte identical.
|
|
157
|
-
5. Decode the canonical 65-byte Base64 compact signature, recover the
|
|
158
|
-
compressed public key from the Bitcoin signed-message hash, and derive
|
|
159
|
-
the network-appropriate BCH CashAddr. It must equal the bound address.
|
|
102
|
+
`bch-proof` tag; validate all known optional tags and reject duplicates.
|
|
103
|
+
3. Require canonical content with the exact v3 fields, timestamp, binding ID,
|
|
104
|
+
event tags, and resolved identity to agree.
|
|
105
|
+
4. Recompute the binding ID and exact v3 BCH signing payload.
|
|
106
|
+
5. Recover the compressed BCH public key from the canonical compact signature
|
|
107
|
+
and require its CashAddr to equal the bound address.
|
|
160
108
|
6. Report `verified` only when both the Nostr and BCH signatures are valid.
|
|
161
109
|
|
|
162
|
-
V1
|
|
163
|
-
fallbacks.
|
|
110
|
+
V1 and v2 bindings MUST NOT be accepted as fallbacks.
|
|
164
111
|
|
|
165
|
-
##
|
|
112
|
+
## 5. Cross-key rotation
|
|
166
113
|
|
|
167
|
-
|
|
114
|
+
A successor sets `previousBindingId` and includes the matching `supersedes`
|
|
115
|
+
tag. Resolvers MUST verify every event in the chain and fail closed if a
|
|
116
|
+
predecessor is missing, revoked, or invalid.
|
|
168
117
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
118
|
+
When the Nostr key changes, the successor MUST embed a kind `30080`
|
|
119
|
+
authorization event signed by the previous Nostr key. Its canonical content
|
|
120
|
+
names the previous binding ID/key and the exact successor binding ID/key,
|
|
121
|
+
BCH address/network, and creation time. The successor includes
|
|
122
|
+
`["rotation-proof", "<authorization event id>", "<old pubkey>"]`. A resolver
|
|
123
|
+
MUST verify the old-key event signature and match every field and tag to the
|
|
124
|
+
successor. Same-key succession does not need this extra signature. Lost-key
|
|
125
|
+
recovery is out of scope; without old-key authorization, cross-key rotation is
|
|
126
|
+
rejected.
|
|
172
127
|
|
|
173
|
-
|
|
128
|
+
## 6. Dual-key challenge-response authentication
|
|
129
|
+
|
|
130
|
+
The BCH-signed payload is either:
|
|
174
131
|
|
|
175
132
|
```text
|
|
176
|
-
cashlet-did/challenge/
|
|
133
|
+
cashlet-did/challenge/v3.0.0\n<nonce>
|
|
134
|
+
cashlet-did/challenge/v3.0.0\n<nonce>\n<audience>
|
|
177
135
|
```
|
|
178
136
|
|
|
179
|
-
The
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`issuedAt` is provided—a 5-minute TTL. Applications must issue and consume
|
|
186
|
-
one-time challenges and separately verify Nostr control if required.
|
|
187
|
-
|
|
188
|
-
## 8. Key rotation
|
|
137
|
+
The audience form has exactly three lines; no extra line or trailing newline
|
|
138
|
+
is allowed. The Nostr key also signs a kind `27235` event with exactly one each
|
|
139
|
+
of these tags: `u` (verification endpoint), `method` (`POST`), `audience`,
|
|
140
|
+
`challenge`, and `expiration`. Its content is the exact BCH payload above.
|
|
141
|
+
Both proofs MUST match the same verified binding's BCH address and Nostr
|
|
142
|
+
pubkey, nonce, audience, endpoint, issue time, and expiration.
|
|
189
143
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
144
|
+
The server MUST issue an unpredictable, one-time challenge, persist it, and
|
|
145
|
+
atomically consume it only after validating both signatures. Browser-local
|
|
146
|
+
challenge state alone is not server-enforced authentication.
|
|
147
|
+
`verifyDualChallengeProof` checks cryptographic and context binding; the
|
|
148
|
+
application is responsible for durable single-use challenge state.
|
|
195
149
|
|
|
196
|
-
##
|
|
150
|
+
## 7. NIP-05 and NIP-98
|
|
197
151
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
SHA-256("cashlet-did/attr/v2\n" + JSON([key, value, salt]))
|
|
203
|
-
```
|
|
152
|
+
NIP-05 resolution is `user@domain` → `GET
|
|
153
|
+
https://domain/.well-known/nostr.json?name=user` → extract pubkey → resolve
|
|
154
|
+
and verify the v3 binding. A registration endpoint MUST verify the candidate
|
|
155
|
+
event's Nostr signature and BCH ownership proof before reserving a handle.
|
|
204
156
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
157
|
+
NIP-98 uses kind `27235`. The `verifyBch: true` option additionally resolves
|
|
158
|
+
the signer's v3 Cashlet DID binding. The server verifier must receive the
|
|
159
|
+
actual request body (`null` when absent), compare its SHA-256 hash with the
|
|
160
|
+
`payload` tag, reject missing/duplicate/mismatched hashes, and enforce the
|
|
161
|
+
documented 60-second event window.
|
|
208
162
|
|
|
209
|
-
##
|
|
163
|
+
## 8. BCH transaction anchor
|
|
210
164
|
|
|
211
|
-
|
|
165
|
+
The OP_RETURN output commits to:
|
|
212
166
|
|
|
213
167
|
```text
|
|
214
168
|
OP_RETURN <"cashlet-did:" + bindingId>
|
|
215
169
|
```
|
|
216
170
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
171
|
+
`buildAnchorTx` constructs a transaction but does not broadcast it. A binding's
|
|
172
|
+
`anchor` field and `bch-anchor` tag only identify a claimed transaction and
|
|
173
|
+
height; they do not prove inclusion. `verifyBchAnchorProof` checks the exact
|
|
174
|
+
zero-value commitment, txid, Merkle inclusion, BCH ASERT difficulty, proof of
|
|
175
|
+
work, median-time-past, future-time bound, and confirmation depth for a header
|
|
176
|
+
chain extending from an application-trusted checkpoint.
|
|
221
177
|
|
|
222
|
-
|
|
178
|
+
The exported `BCH_MAINNET_CHECKPOINT` is a fixed mainnet trust root. The result
|
|
179
|
+
is **checkpointed SPV**, not full-node validation: proof sources are untrusted,
|
|
180
|
+
the checkpoint must not be fetched from the same endpoint, and the proof does
|
|
181
|
+
not establish that a supplied branch is the globally highest-work branch.
|
|
182
|
+
There is no chipnet checkpoint or proof-fetch adapter in this package, so
|
|
183
|
+
chipnet anchors remain unverifiable unless an application supplies an
|
|
184
|
+
independently reviewed checkpoint and consensus configuration.
|
|
223
185
|
|
|
224
|
-
|
|
225
|
-
extract `pubkey` → resolve the v2 binding via §5.
|
|
186
|
+
## 9. Other helpers
|
|
226
187
|
|
|
227
|
-
|
|
188
|
+
Selective disclosure commits each attribute using an independent random
|
|
189
|
+
32-byte salt:
|
|
228
190
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
SHA-256 hash with the event's `payload` tag. Reject missing, duplicate, or
|
|
233
|
-
mismatched payload hashes and enforce the documented 60-second event window.
|
|
191
|
+
```text
|
|
192
|
+
SHA-256("cashlet-did/attr/v2\n" + JSON([key, value, salt]))
|
|
193
|
+
```
|
|
234
194
|
|
|
235
|
-
|
|
195
|
+
Keep salts private until revealing their associated values; include the value
|
|
196
|
+
and salt in each reveal so a verifier can recompute the commitment. The helper
|
|
197
|
+
does not publish commitments or attach them to a binding event automatically.
|
|
236
198
|
|
|
237
|
-
Credential events use kind `30085`. A resolver must verify the Nostr
|
|
199
|
+
Credential events use kind `30085`. A resolver must verify the issuer's Nostr
|
|
238
200
|
signature, require the event's `p` tag and credential payload subject to match
|
|
239
201
|
the requested pubkey, and require the `credential-type` tag to match the
|
|
240
|
-
payload type. `issuedAt` must match the event timestamp. If `expiry` is
|
|
241
|
-
reject the credential once that Unix timestamp has passed. A
|
|
242
|
-
effective only when a valid kind `5` event from the
|
|
243
|
-
references that credential event
|
|
244
|
-
|
|
202
|
+
payload type. `issuedAt` must match the event timestamp. If `expiry` is
|
|
203
|
+
present, reject the credential once that Unix timestamp has passed. A
|
|
204
|
+
revocation is effective only when a valid kind `5` event from the issuer
|
|
205
|
+
references that credential event. Cryptographic validity does not establish
|
|
206
|
+
issuer trust; that policy is application-specific.
|
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ A standalone, framework-agnostic library implementing the **Cashlet DID protocol
|
|
|
4
4
|
|
|
5
5
|
Any BCH wallet or Nostr client can use this library to publish, resolve, and verify BCH–Nostr identity bindings without depending on the Cashlet extension.
|
|
6
6
|
|
|
7
|
-
This release is Cashlet DID
|
|
8
|
-
do not accept v1 bindings; existing identities must be rebound and
|
|
7
|
+
This release is Cashlet DID v3.0.0, a breaking protocol update. V3 verifiers
|
|
8
|
+
do not accept v1 or v2 bindings; existing identities must be rebound and
|
|
9
|
+
republished.
|
|
9
10
|
|
|
10
11
|
## Real-world use cases
|
|
11
12
|
|
|
@@ -13,9 +14,9 @@ do not accept v1 bindings; existing identities must be rebound and republished.
|
|
|
13
14
|
|
|
14
15
|
**Nostr client** — call `verifyIdentity` when rendering a profile to show a cryptographically-proven "verified BCH address" badge. No issuing authority — the proof is in the signature.
|
|
15
16
|
|
|
16
|
-
**Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time to resolve the current NIP-05 pubkey and its verified binding.
|
|
17
|
+
**Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time to resolve the current NIP-05 pubkey and its verified binding. The v3 resolver checks old-key authorization when following a cross-key rotation.
|
|
17
18
|
|
|
18
|
-
**Passwordless login** —
|
|
19
|
+
**Passwordless login** — issue a server-side one-time challenge, collect BCH and Nostr signatures, and verify them with `verifyDualChallengeProof`. The application must atomically consume challenges and apply its own authorization policy. `verifyHttpAuth` remains available for NIP-98 requests.
|
|
19
20
|
|
|
20
21
|
## Install
|
|
21
22
|
|
|
@@ -110,35 +111,53 @@ const social = deriveNostrKeypair(mnemonic, 1)
|
|
|
110
111
|
### Challenge-response authentication
|
|
111
112
|
|
|
112
113
|
```ts
|
|
113
|
-
import {
|
|
114
|
+
import {
|
|
115
|
+
bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof,
|
|
116
|
+
} from 'did-beta-bch'
|
|
114
117
|
|
|
115
118
|
const payload = bchChallengePayload(nonce, audience)
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const nostrTemplate = nostrChallengeTemplate(
|
|
120
|
+
nonce, audience, 'https://app.example/api/auth/verify', issuedAt,
|
|
121
|
+
)
|
|
122
|
+
// Collect the BCH signature over `payload` and Nostr signature over `nostrTemplate`.
|
|
123
|
+
const valid = verifyDualChallengeProof(
|
|
124
|
+
{ signature, payload, address, nostrEvent },
|
|
125
|
+
{
|
|
126
|
+
nonce,
|
|
127
|
+
audience,
|
|
128
|
+
endpoint: 'https://app.example/api/auth/verify',
|
|
129
|
+
issuedAt,
|
|
130
|
+
address,
|
|
131
|
+
pubkey,
|
|
132
|
+
},
|
|
123
133
|
)
|
|
124
134
|
```
|
|
125
135
|
|
|
136
|
+
Challenge issuance and one-time consumption belong on a server with durable,
|
|
137
|
+
atomic storage. Do not treat browser-only challenge state as authentication.
|
|
138
|
+
|
|
126
139
|
### Key rotation
|
|
127
140
|
|
|
128
141
|
```ts
|
|
129
|
-
import {
|
|
142
|
+
import {
|
|
143
|
+
buildBinding, rotationAuthorizationTemplate, nostrEventTemplate,
|
|
144
|
+
resolveIdentityChain,
|
|
145
|
+
} from 'did-beta-bch'
|
|
130
146
|
|
|
131
|
-
//
|
|
147
|
+
// For a changed Nostr key, have the old key sign this authorization event.
|
|
132
148
|
const newBinding = await buildBinding(newAddress, 'mainnet', newNostrPub, undefined, oldBindingId)
|
|
149
|
+
const authorizationTemplate = rotationAuthorizationTemplate(oldBinding, newBinding)
|
|
150
|
+
const oldKeyAuthorization = finalizeEvent(authorizationTemplate, oldNostrPrivkey)
|
|
151
|
+
// Publish the successor with its BCH proof and the old-key authorization.
|
|
152
|
+
const successorTemplate = nostrEventTemplate(newBinding, bchProof, oldKeyAuthorization)
|
|
133
153
|
|
|
134
154
|
// Resolve linked, individually verified binding events, newest first
|
|
135
155
|
const chain = await resolveIdentityChain('npub1...')
|
|
136
156
|
```
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
present, unrevoked, and valid.
|
|
140
|
-
|
|
141
|
-
chain alone as proof of cross-key continuity.
|
|
158
|
+
For cross-key rotation, the resolver verifies the old-key authorization and
|
|
159
|
+
requires every `supersedes` target to be present, unrevoked, and valid. Lost-key
|
|
160
|
+
recovery is not supported.
|
|
142
161
|
|
|
143
162
|
### Verifiable credentials + reputation
|
|
144
163
|
|
|
@@ -178,16 +197,52 @@ commitments or attach them to a binding event automatically.
|
|
|
178
197
|
### BCH-anchored identity commitment
|
|
179
198
|
|
|
180
199
|
```ts
|
|
181
|
-
import {
|
|
200
|
+
import {
|
|
201
|
+
buildAnchorTx, verifyBchAnchorProof,
|
|
202
|
+
BCH_MAINNET_CHECKPOINT, BCH_MAINNET_ASERT_PARAMETERS,
|
|
203
|
+
BCH_CHIPNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS,
|
|
204
|
+
broadcastAnchorTx, fetchAnchorProof, wsTransport,
|
|
205
|
+
ELECTRUM_MAINNET_WS,
|
|
206
|
+
} from 'did-beta-bch'
|
|
182
207
|
|
|
183
|
-
//
|
|
208
|
+
// Build and broadcast
|
|
184
209
|
const { txHex } = await buildAnchorTx(bindingId, { txid, vout, valueSats, lockingBytecode, privateKey })
|
|
210
|
+
const transport = wsTransport(ELECTRUM_MAINNET_WS)
|
|
211
|
+
const txid = await broadcastAnchorTx(txHex, transport)
|
|
212
|
+
|
|
213
|
+
// Fetch proof and verify (after confirmation)
|
|
214
|
+
const proof = await fetchAnchorProof(txid, BCH_MAINNET_CHECKPOINT.height, wsTransport(ELECTRUM_MAINNET_WS))
|
|
215
|
+
const result = verifyBchAnchorProof(proof, bindingId, {
|
|
216
|
+
checkpoint: BCH_MAINNET_CHECKPOINT,
|
|
217
|
+
asert: BCH_MAINNET_ASERT_PARAMETERS,
|
|
218
|
+
minConfirmations: 6,
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`wsTransport` works in both browser and Node.js (Node 22+ has native WebSocket). For Node < 22 or TCP, implement the three-method `ElectrumTransport` interface.
|
|
223
|
+
|
|
224
|
+
Chipnet uses `BCH_CHIPNET_CHECKPOINT` and `BCH_CHIPNET_ASERT_PARAMETERS`. Note chipnet is a test network that may reset; the checkpoint is valid for the current chain instance only.
|
|
225
|
+
|
|
226
|
+
### BIP47 payment code address derivation
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { parsePaymentCode, derivePaymentAddress } from 'did-beta-bch'
|
|
230
|
+
import { deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode } from '@bitauth/libauth'
|
|
231
|
+
|
|
232
|
+
// Decode a recipient's payment code
|
|
233
|
+
const { publicKey, chainCode } = parsePaymentCode('PM8T...')
|
|
234
|
+
|
|
235
|
+
// Derive the recipient's HD public node from their payment code
|
|
236
|
+
// (or supply it directly if you have it from your own derivation)
|
|
237
|
+
const recipientNode = deriveHdPublicNode(deriveHdPath(recipientRoot, "m/47'/0'/0'"))
|
|
238
|
+
|
|
239
|
+
// Sender: derive the nth payment address to send to
|
|
240
|
+
const senderScanPrivkey = deriveHdPath(senderRoot, "m/47'/0'/0'/0").privateKey
|
|
241
|
+
const address = await derivePaymentAddress(senderScanPrivkey, recipientNode, 0)
|
|
242
|
+
wallet.sendTo(address, amount)
|
|
185
243
|
```
|
|
186
244
|
|
|
187
|
-
|
|
188
|
-
does not broadcast the transaction or verify its confirmation. A `bch-anchor`
|
|
189
|
-
tag is only a claim until an application independently verifies the transaction
|
|
190
|
-
and its block inclusion.
|
|
245
|
+
`derivePaymentAddress` implements BIP47 v1 ECDH address derivation. Notification transactions and spending-side key derivation are wallet-specific and out of scope for this library.
|
|
191
246
|
|
|
192
247
|
### NIP-05 handle resolution
|
|
193
248
|
|
|
@@ -246,17 +301,15 @@ Bindings are published as Nostr events of kind `30078` (NIP-78 addressable data)
|
|
|
246
301
|
| `supersedes` | previous binding ID (key rotation) |
|
|
247
302
|
| `t` | `cashlet-did` |
|
|
248
303
|
|
|
249
|
-
The wallet receives the exact UTF-8
|
|
304
|
+
The wallet receives the exact UTF-8 v3 message text and applies the Bitcoin
|
|
250
305
|
Signed Message prefix, CompactSize byte length, and double-SHA256. Its response
|
|
251
306
|
is standard padded Base64 for a 65-byte compact signature: header 31–34
|
|
252
|
-
(compressed key plus recovery ID 0–3), followed by 64-byte `r || s`.
|
|
253
|
-
verifiers strictly reject v1 hex/raw signature encodings. Revocation uses
|
|
307
|
+
(compressed key plus recovery ID 0–3), followed by 64-byte `r || s`. V3
|
|
308
|
+
verifiers strictly reject v1/v2 and hex/raw signature encodings. Revocation uses
|
|
254
309
|
NIP-09 kind `5` deletion events.
|
|
255
310
|
|
|
256
311
|
The local demo previews and verifies a binding but does **not** publish or query
|
|
257
|
-
Nostr relays
|
|
258
|
-
verification, and replay rejection locally; `/resolve` needs a published
|
|
259
|
-
binding and relay access.
|
|
312
|
+
Nostr relays; `/resolve` needs a published binding and relay access.
|
|
260
313
|
|
|
261
314
|
## Relays
|
|
262
315
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electrum client helpers for broadcasting anchor transactions and fetching
|
|
3
|
+
* SPV proofs. Works in Node.js (TCP) and browsers (WebSocket) via a pluggable
|
|
4
|
+
* transport. Default transports are provided for each environment.
|
|
5
|
+
*/
|
|
6
|
+
import type { BchAnchorProof } from "./anchor-verification.js";
|
|
7
|
+
export declare const ELECTRUM_MAINNET_TCP = "electroncash.dk:50001";
|
|
8
|
+
export declare const ELECTRUM_MAINNET_WS = "wss://electroncash.dk:50004";
|
|
9
|
+
export declare const ELECTRUM_CHIPNET_TCP = "chipnet.imaginary.cash:50001";
|
|
10
|
+
export declare const ELECTRUM_CHIPNET_WS = "wss://chipnet.imaginary.cash:50004";
|
|
11
|
+
/** Minimal transport interface — send lines, receive lines. */
|
|
12
|
+
export interface ElectrumTransport {
|
|
13
|
+
send(line: string): void;
|
|
14
|
+
onLine(handler: (line: string) => void): void;
|
|
15
|
+
onError(handler: (err: Error) => void): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
/** WebSocket transport — works in both browser and Node.js (Node 22+ has native WebSocket). */
|
|
19
|
+
export declare function wsTransport(url: string): ElectrumTransport;
|
|
20
|
+
/**
|
|
21
|
+
* Broadcast a signed anchor transaction hex to an Electrum server.
|
|
22
|
+
* Returns the txid on success.
|
|
23
|
+
*/
|
|
24
|
+
export declare function broadcastAnchorTx(txHex: string, transport: ElectrumTransport): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Fetch a BchAnchorProof for a confirmed transaction.
|
|
27
|
+
* checkpointHeight must match the checkpoint you intend to verify against —
|
|
28
|
+
* headers are fetched from checkpointHeight+1 through the confirmation window.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchAnchorProof(txid: string, checkpointHeight: number, transport: ElectrumTransport, minConfirmations?: number): Promise<BchAnchorProof>;
|