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 CHANGED
@@ -1,75 +1,55 @@
1
1
  # Cashlet DID Protocol Specification
2
2
 
3
- Version: `2.0.0`
4
- Status: Draft
3
+ Version: `3.0.0`
4
+ Status: Final
5
5
 
6
- This is the normative specification for Cashlet DID v2. V2 is a breaking
7
- signature-format update: conforming verifiers MUST reject v1 proof tags and
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 | `2.0.0` |
18
- | Binding event kind | `30078` (NIP-78 addressable data) |
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. Canonical binding record
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
- "address": "<prefixed-cashaddr>",
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-v2",
41
- "version": "2.0.0"
34
+ "purpose": "identity-binding-v3",
35
+ "version": "3.0.0"
42
36
  }
43
37
  ```
44
38
 
45
- **Binding ID** = lowercase hex `SHA-256(canonicalize(record_without_bindingId))`.
46
- Arrays preserve order. `mainnet` addresses use the `bitcoincash:` prefix;
47
- `testnet` and `chipnet` use `bchtest:`.
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 text supplied to a wallet is:
44
+ The exact UTF-8 message supplied to the BCH wallet is:
52
45
 
53
46
  ```text
54
- cashlet-did/bch-proof/v2.0.0\n<canonicalized subset>
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
- Pass this exact message string to the wallet's message-signing method. Do not
72
- pre-hash, add a Bitcoin prefix, or otherwise transform it in the adapter. The
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
- `CompactSize` uses Bitcoin's little-endian encodings: one byte below `0xfd`,
81
- `fd` plus 2 bytes through `0xffff`, `fe` plus 4 bytes through `0xffffffff`,
82
- then `ff` plus 8 bytes.
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
- The wallet response is standard padded Base64 encoding of exactly 65 bytes:
65
+ ## 3. Binding Nostr event
85
66
 
86
- ```text
87
- [header byte 31–34] || [32-byte r] || [32-byte s]
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", "2.0.0"],
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
- ```json
124
- ["bch-payment-code", "<BIP47 code>"] // must equal content.bch.paymentCode
125
- ["supersedes", "<previous bindingId>"] // must equal content.previousBindingId
126
- ["bch-anchor", "<txid>", "<claimed height>"] // unverified chain claim
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
- `content` is the canonicalized JSON object containing `protocol`, `version`,
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
- ## 5. Discovery and revocation
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 v2 binding.
96
+ tags. Filter revoked records before selecting the newest binding.
144
97
 
145
- ## 6. Verification algorithm
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. The proof tag MUST have exactly four elements and the v2
150
- format marker. Check optional payment-code and supersedes tags against
151
- content; reject duplicate or malformed known tags.
152
- 3. Parse content and require its canonical serialization, exact v2 fields,
153
- event timestamp, `d`, `bch`, protocol/version tags, and resolved identity
154
- (`npub`, Nostr pubkey, address, network, and binding ID) to agree.
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 tags do not satisfy the v2 proof-tag structure and MUST NOT be accepted as
163
- fallbacks.
110
+ V1 and v2 bindings MUST NOT be accepted as fallbacks.
164
111
 
165
- ## 7. Challenge-response authentication
112
+ ## 5. Cross-key rotation
166
113
 
167
- The exact v2 message is either:
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
- ```text
170
- cashlet-did/challenge/v2.0.0\n<nonce>
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
- or, with an audience:
128
+ ## 6. Dual-key challenge-response authentication
129
+
130
+ The BCH-signed payload is either:
174
131
 
175
132
  ```text
176
- cashlet-did/challenge/v2.0.0\n<nonce>\n<audience>
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 no-audience form has exactly two lines; the audience form has exactly three.
180
- No extra lines or trailing newline are allowed. The nonce and optional audience
181
- are single-line strings. The BCH wallet signs the exact text using §3's Bitcoin
182
- signed-message hash and 65-byte Base64 signature.
183
- `bchChallengePayload(nonce, audience?)` constructs the payload;
184
- `verifyChallengeProof` checks exact equality, signature, address, and—when
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
- Set `previousBindingId` in the new binding and add the matching
191
- `["supersedes", "<previousBindingId>"]` tag. Resolvers can follow references
192
- backwards, but must independently verify each referenced binding and fail
193
- closed if a predecessor is unavailable or invalid. A supersedes tag authored
194
- by the new key does not prove that the previous Nostr key authorized rotation.
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
- ## 9. Selective disclosure
150
+ ## 7. NIP-05 and NIP-98
197
151
 
198
- The library's selective-disclosure helper commits each attribute using an
199
- independent random 32-byte salt:
200
-
201
- ```text
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
- Keep salts private until revealing their associated values; include the value
206
- and salt in each reveal so a verifier can recompute the commitment. The helper
207
- does not publish commitments or attach them to a binding event automatically.
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
- ## 10. On-chain commitment (OP_RETURN)
163
+ ## 8. BCH transaction anchor
210
164
 
211
- Embed the binding ID in a BCH transaction output:
165
+ The OP_RETURN output commits to:
212
166
 
213
167
  ```text
214
168
  OP_RETURN <"cashlet-did:" + bindingId>
215
169
  ```
216
170
 
217
- The library can construct an anchor transaction, but it does not broadcast the
218
- transaction or verify its inclusion in a confirmed block. A `bch-anchor` event
219
- tag is only an issuer-provided claim; applications that need confirmed
220
- anchoring must independently verify the transaction and block inclusion.
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
- ## 11. NIP-05 handle resolution
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
- `user@domain` → `GET https://domain/.well-known/nostr.json?name=user` →
225
- extract `pubkey` → resolve the v2 binding via §5.
186
+ ## 9. Other helpers
226
187
 
227
- ## 12. NIP-98 HTTP Auth
188
+ Selective disclosure commits each attribute using an independent random
189
+ 32-byte salt:
228
190
 
229
- Build a kind `27235` event per NIP-98. The `verifyBch: true` option on the
230
- server additionally resolves the signer's v2 Cashlet DID binding. The verifier
231
- must receive the actual request body (`null` when absent) and compare its
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
- ## 13. Credentials
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 event
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 present,
241
- reject the credential once that Unix timestamp has passed. A revocation is
242
- effective only when a valid kind `5` event from the credential's issuer
243
- references that credential event ID. Cryptographic validity does not establish
244
- that an issuer is trusted; trust policy is application-specific.
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 v2.0.0, a breaking proof-format update. V2 verifiers
8
- do not accept v1 bindings; existing identities must be rebound and republished.
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. Cross-key rotation continuity is not established by this library, so merchants must update their NIP-05 record and clients must not assume a new key is authorized by the old one.
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** — use `verifyHttpAuth` in a server with a NIP-07 signer. The server verifies the signed Nostr request and can optionally resolve the signer's BCH binding; it still needs application-specific authorization and replay policy.
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 { bchChallengePayload, signBchMessage, verifyChallengeProof } from 'did-beta-bch'
114
+ import {
115
+ bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof,
116
+ } from 'did-beta-bch'
114
117
 
115
118
  const payload = bchChallengePayload(nonce, audience)
116
- const signature = await signBchMessage(walletSigner, payload)
117
- // issuedAt enforces 5-minute TTL
118
- const valid = verifyChallengeProof(
119
- { signature, payload, address },
120
- nonce,
121
- audience,
122
- issuedAt, // unix seconds — optional, rejects if > 5 min old
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 { buildBinding, resolveIdentityChain } from 'did-beta-bch'
142
+ import {
143
+ buildBinding, rotationAuthorizationTemplate, nostrEventTemplate,
144
+ resolveIdentityChain,
145
+ } from 'did-beta-bch'
130
146
 
131
- // Rotate: reference the old binding
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
- The resolver verifies each event and requires every `supersedes` target to be
139
- present, unrevoked, and valid. A reference signed only by the new key does not
140
- prove that the previous Nostr key authorized the rotation; do not treat this
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 { buildAnchorTx } from 'did-beta-bch'
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
- // Embed bindingId in an OP_RETURN transaction for on-chain commitment
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
- This builds transaction hex from the supplied UTXO; it
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 v2 message text and applies the Bitcoin
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`. V2
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. Its separate HTTP verifier demo demonstrates challenge issue,
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>;