did-beta-bch 1.3.0 → 2.0.1

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
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `cashlet-did` will be documented here.
4
4
 
5
+ ## [2.0.0] — 2026-11-03
6
+
7
+ ### Breaking
8
+ - V2 is the only supported binding protocol; v1 bindings and raw/hex proof signatures are no longer verified.
9
+ - BCH proof tags now use `["bch-proof", "bitcoin-message/compact-base64", message, signatureBase64]`.
10
+ - Wallet signatures must be standard padded Base64 for a 65-byte Bitcoin compact-message signature with compressed-key header 31–34.
11
+ - Binding purpose and BCH proof domain are versioned to `identity-binding-v2` and `cashlet-did/bch-proof/v2.0.0`.
12
+ - Challenge payloads are versioned as `cashlet-did/challenge/v2.0.0\n<nonce>[\n<audience>]`.
13
+ - `nostrEventTemplate` accepts `{ message, signatureBase64 }` and sets event `created_at` to the binding timestamp.
14
+
15
+ ### Added
16
+ - `BCH_PROOF_FORMAT`, `BchMessageSigner`, `normalizeBchMessageSignature`, and `signBchMessage` provide strict transport-independent signing validation.
17
+ - `bchChallengePayload(nonce, audience?)` constructs the exact challenge text expected by `verifyChallengeProof`.
18
+ - Verifier now binds v2 content, proof, tags, event time, Nostr key/npub, BCH address, network, and binding ID; resolution skips v1 events.
19
+
5
20
  ## [1.2.0] — 2026-07-31
6
21
 
7
22
  ### Fixed
@@ -28,7 +43,7 @@ All notable changes to `cashlet-did` will be documented here.
28
43
 
29
44
  ### Added
30
45
  - `buildAnchorTx(bindingId, utxo)` — build an OP_RETURN BCH transaction anchoring a binding to the chain
31
- - `issueCredential(subjectNpub, type, issuerPrivkey)` — issue a third-party Nostr attestation (kind 30079)
46
+ - `issueCredential(subjectNpub, type, issuerPrivkey)` — issue a third-party Nostr attestation (kind 30085)
32
47
  - `resolveCredentials(npub)` — fetch all credentials issued to an npub from relays
33
48
  - `buildDisclosure(attrs)` — commit to a set of attributes with individual SHA-256 hashes
34
49
  - `verifyDisclosure(revealed, commitments)` — verify selectively revealed attributes against commitments
package/PROTOCOL.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Cashlet DID Protocol Specification
2
2
 
3
- Version: `1.0.0`
3
+ Version: `2.0.0`
4
4
  Status: Draft
5
5
 
6
- This document is the normative protocol specification. Any BCH wallet or Nostr
7
- client can implement compatibility by following this spec — no dependency on the
8
- `cashlet-did` library is required.
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
9
 
10
10
  ---
11
11
 
@@ -14,201 +14,231 @@ client can implement compatibility by following this spec — no dependency on t
14
14
  | Constant | Value |
15
15
  |---|---|
16
16
  | Protocol ID | `cashlet-did` |
17
- | Protocol version | `1.0.0` |
17
+ | Protocol version | `2.0.0` |
18
18
  | Binding event kind | `30078` (NIP-78 addressable data) |
19
19
  | Credential event kind | `30085` |
20
20
  | Revocation event kind | `5` (NIP-09) |
21
21
  | Nostr derivation path | `m/44'/1237'/<account>'/0/0` (NIP-06) |
22
22
 
23
- ---
24
-
25
23
  ## 2. Canonical binding record
26
24
 
27
- A binding record is a JSON object with **sorted keys** (no whitespace) used as
28
- the input to the binding ID hash. Undefined/absent fields are omitted.
25
+ A binding record has recursively sorted object keys, no whitespace, and omits
26
+ undefined/absent fields:
29
27
 
30
28
  ```json
31
29
  {
32
30
  "bch": {
33
- "address": "<cashaddr>",
31
+ "address": "<prefixed-cashaddr>",
34
32
  "network": "mainnet" | "testnet" | "chipnet",
35
33
  "paymentCode": "<BIP47 payment code>" // optional
36
34
  },
37
35
  "createdAt": <unix seconds>,
38
- "nostr": { "pubkey": "<32-byte hex>" },
39
- "nonce": "<64 hex chars of random bytes>",
40
- "previousBindingId": "<hex>", // optional, key rotation
36
+ "nostr": { "pubkey": "<32-byte lowercase hex>" },
37
+ "nonce": "<64 lowercase hex chars>",
38
+ "previousBindingId": "<64 lowercase hex chars>", // optional
41
39
  "protocol": "cashlet-did",
42
- "purpose": "identity-binding-v1",
43
- "version": "1.0.0"
40
+ "purpose": "identity-binding-v2",
41
+ "version": "2.0.0"
44
42
  }
45
43
  ```
46
44
 
47
- **Binding ID** = `SHA-256(canonicalize(record_without_bindingId))` as lowercase hex.
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
48
 
49
- `canonicalize` means: recursively sort object keys alphabetically, no spaces,
50
- omit keys whose value is `undefined`. Arrays preserve order.
49
+ ## 3. BCH signing payload and wallet contract
51
50
 
52
- ---
51
+ The exact UTF-8 message text supplied to a wallet is:
53
52
 
54
- ## 3. BCH signing payload
55
-
56
- ```
57
- cashlet-did/bch-proof/v1.0.0\n<canonicalized subset>
53
+ ```text
54
+ cashlet-did/bch-proof/v2.0.0\n<canonicalized subset>
58
55
  ```
59
56
 
60
- The canonicalized subset contains exactly these keys (sorted):
57
+ The subset contains exactly these keys:
61
58
 
62
59
  ```json
63
60
  {
64
- "bchAddress": "<cashaddr>",
61
+ "bchAddress": "<prefixed-cashaddr>",
65
62
  "bchNetwork": "mainnet" | "testnet" | "chipnet",
66
- "bindingId": "<hex>",
63
+ "bindingId": "<64 lowercase hex chars>",
67
64
  "createdAt": <unix seconds>,
68
- "nonce": "<hex>",
69
- "nostrPubkey": "<hex>",
70
- "purpose": "identity-binding-v1"
65
+ "nonce": "<64 lowercase hex chars>",
66
+ "nostrPubkey": "<32-byte lowercase hex>",
67
+ "purpose": "identity-binding-v2"
71
68
  }
72
69
  ```
73
70
 
74
- ### BCH signature format
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
73
+ wallet computes:
75
74
 
76
- Bitcoin signed message format:
77
-
78
- ```
79
- SHA256d( "\x18Bitcoin Signed Message:\n" + varint(len(payload_bytes)) + payload_bytes )
75
+ ```text
76
+ SHA256d(0x18 || "Bitcoin Signed Message:\n" ||
77
+ CompactSize(UTF8(message).length) || UTF8(message))
80
78
  ```
81
79
 
82
- `varint` is Bitcoin compact-size encoding:
83
- - `n < 0xfd` → 1 byte `[n]`
84
- - `0xfd ≤ n ≤ 0xffff` → 3 bytes `[0xfd, n & 0xff, n >> 8]`
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.
85
83
 
86
- The signature is a **65-byte recoverable compact signature**:
87
- `[recovery_id_byte (0–3)] + [64-byte compact sig]`, hex-encoded.
84
+ The wallet response is standard padded Base64 encoding of exactly 65 bytes:
88
85
 
89
- > ⚠️ Signing payloads are typically ~400 bytes. A single-byte length prefix
90
- > (truncating to `n & 0xff`) is **wrong** and will produce unverifiable signatures.
86
+ ```text
87
+ [header byte 31–34] || [32-byte r] || [32-byte s]
88
+ ```
91
89
 
92
- ---
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.
93
106
 
94
107
  ## 4. Nostr event (kind 30078)
95
108
 
109
+ The event timestamp MUST equal the binding's `createdAt`. Required tags:
110
+
96
111
  ```json
97
- {
98
- "kind": 30078,
99
- "pubkey": "<nostr pubkey hex>",
100
- "created_at": <unix seconds>,
101
- "tags": [
102
- ["d", "<bindingId>"],
103
- ["bch", "<cashaddr>", "<network>"],
104
- ["bch-proof", "<signing payload>", "<65-byte sig hex>"],
105
- ["protocol", "cashlet-did", "1.0.0"],
106
- ["t", "cashlet-did"],
107
- ["bch-payment-code", "<BIP47 code>"], // optional
108
- ["supersedes", "<previous bindingId>"], // optional, key rotation
109
- ["bch-anchor", "<txid>", "<block timestamp unix>"] // optional
110
- ],
111
- "content": "<canonicalized binding JSON>",
112
- "id": "<NIP-01 event id>",
113
- "sig": "<Schnorr sig>"
114
- }
112
+ [
113
+ ["d", "<bindingId>"],
114
+ ["bch", "<cashaddr>", "<network>"],
115
+ ["protocol", "cashlet-did", "2.0.0"],
116
+ ["t", "cashlet-did"],
117
+ ["bch-proof", "bitcoin-message/compact-base64", "<exact message>", "<signature Base64>"]
118
+ ]
115
119
  ```
116
120
 
117
- ---
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
+ ```
128
+
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.
133
+
134
+ ## 5. Discovery and revocation
118
135
 
119
- ## 5. Discovery (NIP-01 filter)
136
+ Discover v2 records with this NIP-01 filter:
120
137
 
121
138
  ```json
122
139
  { "kinds": [30078], "authors": ["<pubkey hex>"], "#t": ["cashlet-did"] }
123
140
  ```
124
141
 
125
- Query any public Nostr relay. Default relays: `wss://nos.lol`,
126
- `wss://relay.damus.io`, `wss://relay.nostr.band`.
127
-
128
142
  Revocations are kind `5` events from the same author with `["e", "<event id>"]`
129
- tags. Filter revoked events before selecting the latest binding.
130
-
131
- ---
143
+ tags. Filter revoked records before selecting the newest v2 binding.
132
144
 
133
145
  ## 6. Verification algorithm
134
146
 
135
- 1. Fetch binding event(s) via NIP-01 filter above.
136
- 2. Fetch kind `5` revocations from the same author; discard revoked events.
137
- 3. Select the event with the highest `created_at` among non-revoked results.
138
- 4. Verify the **Nostr event signature** (NIP-01 Schnorr).
139
- 5. Reconstruct the signing payload from the `bch-proof` tag (index 1).
140
- 6. Compute `SHA256d` of the Bitcoin signed message over that payload (§3).
141
- 7. Recover the BCH public key from the 65-byte signature + hash.
142
- 8. Derive the P2PKH address from the recovered key; compare to the `bch` tag.
143
- 9. Status is `verified` iff both signatures are valid.
144
-
145
- ---
147
+ 1. Verify the NIP-01 event signature and require kind `30078`.
148
+ 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.
160
+ 6. Report `verified` only when both the Nostr and BCH signatures are valid.
161
+
162
+ V1 tags do not satisfy the v2 proof-tag structure and MUST NOT be accepted as
163
+ fallbacks.
146
164
 
147
165
  ## 7. Challenge-response authentication
148
166
 
149
- Payload format:
167
+ The exact v2 message is either:
150
168
 
151
- ```
152
- cashlet-did/challenge\n<nonce>\n<audience>
169
+ ```text
170
+ cashlet-did/challenge/v2.0.0\n<nonce>
153
171
  ```
154
172
 
155
- - `nonce`: random hex string issued by the verifier
156
- - `audience`: verifier's URL or identifier (optional but recommended)
173
+ or, with an audience:
157
174
 
158
- The wallet signs this payload using the same Bitcoin signed message format (§3).
159
- The verifier recovers the BCH address and checks it matches the binding.
175
+ ```text
176
+ cashlet-did/challenge/v2.0.0\n<nonce>\n<audience>
177
+ ```
160
178
 
161
- Implementations **should** enforce a 5-minute TTL on issued challenges.
162
-
163
- ---
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.
164
187
 
165
188
  ## 8. Key rotation
166
189
 
167
- Set `previousBindingId` in the new binding record and add a
168
- `["supersedes", "<previousBindingId>"]` tag to the Nostr event.
169
-
170
- Resolvers can reconstruct the full rotation chain by following `supersedes` tags
171
- backwards. The newest non-revoked binding is the authoritative identity.
172
-
173
- ---
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.
174
195
 
175
196
  ## 9. Selective disclosure
176
197
 
177
- Each attribute is committed as:
198
+ The library's selective-disclosure helper commits each attribute using an
199
+ independent random 32-byte salt:
178
200
 
179
- ```
180
- SHA-256( "cashlet-did/attr/<key>:<value>" )
201
+ ```text
202
+ SHA-256("cashlet-did/attr/v2\n" + JSON([key, value, salt]))
181
203
  ```
182
204
 
183
- Commitments are published in the binding (e.g. as extra tags or in content).
184
- To verify a revealed attribute, recompute the hash and compare to the commitment.
185
-
186
- ---
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.
187
208
 
188
209
  ## 10. On-chain commitment (OP_RETURN)
189
210
 
190
211
  Embed the binding ID in a BCH transaction output:
191
212
 
192
- ```
213
+ ```text
193
214
  OP_RETURN <"cashlet-did:" + bindingId>
194
215
  ```
195
216
 
196
- The confirmed block's `time` field provides a consensus-enforced timestamp
197
- accurate to ~1 block (~10 minutes). It cannot be significantly backdated
198
- (median-time-past rule) and can drift at most 2 hours forward (miner clock
199
- skew allowance). Prefer `anchoredAt` over the self-reported `createdAt`.
200
-
201
- ---
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.
202
221
 
203
222
  ## 11. NIP-05 handle resolution
204
223
 
205
- `user@domain` → `GET https://domain/.well-known/nostr.json?name=user`
206
- → extract `pubkey` → resolve binding via §5.
207
-
208
- ---
224
+ `user@domain` → `GET https://domain/.well-known/nostr.json?name=user` →
225
+ extract `pubkey` → resolve the v2 binding via §5.
209
226
 
210
227
  ## 12. NIP-98 HTTP Auth
211
228
 
212
229
  Build a kind `27235` event per NIP-98. The `verifyBch: true` option on the
213
- server additionally resolves the signer's cashlet-did binding, proving BCH
214
- address ownership in the same request.
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.
234
+
235
+ ## 13. Credentials
236
+
237
+ Credential events use kind `30085`. A resolver must verify the Nostr event
238
+ signature, require the event's `p` tag and credential payload subject to match
239
+ 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.
package/README.md CHANGED
@@ -4,15 +4,18 @@ 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.
9
+
7
10
  ## Real-world use cases
8
11
 
9
12
  **BCH wallet** — call `buildBinding` + `publishToRelays` once when a user sets up their identity. Anyone can then pay them by Nostr handle (`resolveAddress('npub1...')`) instead of exchanging addresses. Users never see the protocol.
10
13
 
11
14
  **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.
12
15
 
13
- **Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time. No QR codes, no address copy-paste. When the merchant rotates keys, all payers automatically resolve the new address.
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.
14
17
 
15
- **Passwordless login** — drop `verifyHttpAuth` into any web server. A NIP-07 browser extension (Alby, nos2x) signs the request transparently. The server gets a verified Nostr pubkey + BCH address — no OAuth, no email, no password database.
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.
16
19
 
17
20
  ## Install
18
21
 
@@ -28,8 +31,8 @@ npm install did-beta-bch
28
31
  import { resolveAddress } from 'did-beta-bch'
29
32
 
30
33
  const address = await resolveAddress('npub1...')
31
- // Returns a token-aware bitcoincash:z... address
32
- // Works for BCH, CashTokens (FT), and NFTs — no conversion needed
34
+ // Returns the BCH address published in the verified binding, or null
35
+ // Token support depends on the bound address and the wallet's payment flow
33
36
  if (address) wallet.sendTo(address, amount)
34
37
  ```
35
38
 
@@ -42,26 +45,58 @@ const identity = await resolveIdentity('npub1...')
42
45
  if (identity) {
43
46
  const result = await verifyIdentity(identity)
44
47
  console.log(result.status) // "verified"
45
- console.log(identity.bchAddress) // bitcoincash:z...
48
+ console.log(identity.bchAddress) // bound BCH address
46
49
  }
47
50
  ```
48
51
 
49
52
  ### Build and publish a binding (wallet integration)
50
53
 
51
54
  ```ts
52
- import { buildBinding, bchSigningPayload, nostrEventTemplate, publishToRelays, DEFAULT_RELAYS } from 'did-beta-bch'
55
+ import {
56
+ buildBinding, bchSigningPayload, signBchMessage, nostrEventTemplate,
57
+ publishToRelays, DEFAULT_RELAYS,
58
+ } from 'did-beta-bch'
53
59
  import { finalizeEvent } from 'nostr-tools'
54
60
 
55
61
  const binding = await buildBinding(bchAddress, 'mainnet', nostrPubkeyHex)
56
62
  const message = bchSigningPayload(binding)
57
63
 
58
- // Sign with BCH private key (recoverable compact sig)
59
- const bchSig = signWithBchKey(message) // your wallet's signing function
64
+ // Pass the exact text to a transport adapter; do not pre-hash or add a prefix.
65
+ const signatureBase64 = await signBchMessage(walletSigner, message)
60
66
 
61
- const event = finalizeEvent(nostrEventTemplate(binding, { message, signature: bchSig }), nostrPrivkey)
67
+ const event = finalizeEvent(
68
+ nostrEventTemplate(binding, { message, signatureBase64 }),
69
+ nostrPrivkey,
70
+ )
62
71
  await publishToRelays(DEFAULT_RELAYS, event)
63
72
  ```
64
73
 
74
+ `BchMessageSigner` is transport-neutral:
75
+
76
+ ```ts
77
+ const walletSigner = {
78
+ signMessage: (message: string) => walletAdapter.signMessage({ message }),
79
+ }
80
+ ```
81
+
82
+ `signBchMessage` passes the exact string to `signMessage` and strictly validates
83
+ the standard 65-byte Bitcoin compact-message signature returned as Base64.
84
+ The response must use a compressed-key header byte from 31 through 34. Paytaca's
85
+ published WalletConnect implementation and the BCH WalletConnect BCR match this
86
+ contract; test each wallet version you integrate. The library does not import
87
+ WalletConnect or WizardConnect and does not provide their transport adapters.
88
+ For WalletConnect, the adapter maps this call to `bch_signMessage` with the
89
+ message in its params. WizardConnect needs a separate wallet-side signing action
90
+ and adapter; neither is implemented in this package.
91
+
92
+ The signer is supplied by the integrating wallet, so this library does not
93
+ require users to connect to Selene, Paytaca, or any other external wallet. A
94
+ wallet can pass its own non-custodial BCH signer directly. For example, Cashlet
95
+ uses its own BCH keys by default; its optional WalletConnect adapter is only for
96
+ users who choose to bind an address held by another compatible wallet. In either
97
+ case, the BCH key signs the proof while a separate Nostr key signs the published
98
+ event.
99
+
65
100
  ### Key derivation (NIP-06)
66
101
 
67
102
  ```ts
@@ -75,9 +110,10 @@ const social = deriveNostrKeypair(mnemonic, 1)
75
110
  ### Challenge-response authentication
76
111
 
77
112
  ```ts
78
- import { verifyChallengeProof } from 'did-beta-bch'
113
+ import { bchChallengePayload, signBchMessage, verifyChallengeProof } from 'did-beta-bch'
79
114
 
80
- // Payload format: `cashlet-did/challenge\n<nonce>\n<audience>`
115
+ const payload = bchChallengePayload(nonce, audience)
116
+ const signature = await signBchMessage(walletSigner, payload)
81
117
  // issuedAt enforces 5-minute TTL
82
118
  const valid = verifyChallengeProof(
83
119
  { signature, payload, address },
@@ -95,10 +131,15 @@ import { buildBinding, resolveIdentityChain } from 'did-beta-bch'
95
131
  // Rotate: reference the old binding
96
132
  const newBinding = await buildBinding(newAddress, 'mainnet', newNostrPub, undefined, oldBindingId)
97
133
 
98
- // Resolve full rotation history, newest first
134
+ // Resolve linked, individually verified binding events, newest first
99
135
  const chain = await resolveIdentityChain('npub1...')
100
136
  ```
101
137
 
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.
142
+
102
143
  ### Verifiable credentials + reputation
103
144
 
104
145
  ```ts
@@ -112,19 +153,28 @@ const creds = await resolveCredentials('npub1...')
112
153
  const score = await scoreReputation('npub1...')
113
154
  ```
114
155
 
156
+ `resolveCredentials` verifies the issuer's Nostr event signature, the subject
157
+ and credential tags, and any expiry. Applications must still decide which
158
+ issuers and credential types they trust; `scoreReputation` is a heuristic, not
159
+ an objective or Sybil-resistant trust measure.
160
+
115
161
  ### Selective disclosure
116
162
 
117
163
  ```ts
118
164
  import { buildDisclosure, verifyDisclosure } from 'did-beta-bch'
119
165
 
120
- // Commit to attributes — publish commitments in binding
166
+ // Commit to attributes; store commitments with the identity record separately
121
167
  const d = await buildDisclosure({ merchant: 'true', country: 'AU', kyc: 'passed' })
122
168
 
123
- // Later: reveal only what's needed
169
+ // Later: reveal selected values and their salts; hidden salts stay private
124
170
  const revealed = d.reveal(['merchant'])
125
171
  const { verified, failed } = await verifyDisclosure(revealed, d.commitments)
126
172
  ```
127
173
 
174
+ Commitments use random per-attribute salts to prevent offline guessing of
175
+ low-entropy values until those values are revealed. This helper does not publish
176
+ commitments or attach them to a binding event automatically.
177
+
128
178
  ### BCH-anchored identity commitment
129
179
 
130
180
  ```ts
@@ -134,6 +184,11 @@ import { buildAnchorTx } from 'did-beta-bch'
134
184
  const { txHex } = await buildAnchorTx(bindingId, { txid, vout, valueSats, lockingBytecode, privateKey })
135
185
  ```
136
186
 
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.
191
+
137
192
  ### NIP-05 handle resolution
138
193
 
139
194
  ```ts
@@ -148,16 +203,15 @@ if (address) wallet.sendTo(address, amount)
148
203
 
149
204
  ```ts
150
205
  import { buildHttpAuthEventAsync, verifyHttpAuth } from 'did-beta-bch'
151
- import { finalizeEvent } from 'nostr-tools'
152
206
 
153
207
  // Client: build + sign auth event
154
208
  const template = await buildHttpAuthEventAsync('https://api.example.com/me', 'GET')
155
- const event = finalizeEvent(template, nostrPrivkey)
209
+ const event = await window.nostr.signEvent(template) // NIP-07 browser extension
156
210
  const header = 'Nostr ' + btoa(JSON.stringify(event))
157
211
  // → send as Authorization header
158
212
 
159
213
  // Server: verify — proves Nostr identity + optionally BCH address ownership
160
- const result = await verifyHttpAuth(header, 'https://api.example.com/me', 'GET', { verifyBch: true })
214
+ const result = await verifyHttpAuth(header, 'https://api.example.com/me', 'GET', { body: null, verifyBch: true })
161
215
  if (result.valid) {
162
216
  console.log(result.pubkey) // verified Nostr pubkey
163
217
  console.log(result.bchAddress) // verified BCH address (if verifyBch: true)
@@ -186,13 +240,23 @@ Bindings are published as Nostr events of kind `30078` (NIP-78 addressable data)
186
240
  |---|---|
187
241
  | `d` | binding ID (SHA-256 of canonical binding JSON) |
188
242
  | `bch` | BCH address, network |
189
- | `bch-proof` | signing payload, 65-byte recoverable compact signature |
190
- | `bch-payment-code` | BIP47 payment code (optional, enables fresh address per payment) |
243
+ | `bch-proof` | `["bch-proof", "bitcoin-message/compact-base64", message, signatureBase64]` |
244
+ | `bch-payment-code` | Optional BIP47 payment-code metadata; this library does not derive fresh addresses |
191
245
  | `protocol` | `cashlet-did`, version |
192
246
  | `supersedes` | previous binding ID (key rotation) |
193
247
  | `t` | `cashlet-did` |
194
248
 
195
- The BCH signature uses Bitcoin signed message format (`\x18Bitcoin Signed Message:\n`) with a 65-byte recoverable compact signature (recovery ID byte + 64-byte compact sig). Revocation uses NIP-09 kind `5` deletion events.
249
+ The wallet receives the exact UTF-8 v2 message text and applies the Bitcoin
250
+ Signed Message prefix, CompactSize byte length, and double-SHA256. Its response
251
+ 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
254
+ NIP-09 kind `5` deletion events.
255
+
256
+ 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.
196
260
 
197
261
  ## Relays
198
262
 
package/dist/anchor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * BCH on-chain identity commitment via OP_RETURN.
3
- * Anchors a bindingId to a BCH block, providing a tamper-evident timestamp.
3
+ * Builds a transaction committing to a bindingId; it does not broadcast or verify confirmation.
4
4
  */
5
5
  export declare const ANCHOR_PREFIX = "cashlet-did:";
6
6
  export interface AnchorUtxo {