@did-btcr2/method 0.49.0 → 0.51.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/README.md +13 -13
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -41538
- package/dist/browser.mjs +3 -41532
- package/dist/cjs/index.js +74 -27
- package/dist/esm/core/beacon/beacon.js +74 -8
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/did-sender-resolver.js +74 -9
- package/dist/esm/core/did-sender-resolver.js.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +44 -0
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/did-sender-resolver.d.ts +49 -9
- package/dist/types/core/did-sender-resolver.d.ts.map +1 -1
- package/package.json +5 -19
- package/src/core/beacon/beacon.ts +82 -12
- package/src/core/did-sender-resolver.ts +90 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@did-btcr2/method",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Reference implementation for the did:btcr2 DID method written in TypeScript and JavaScript. did:btcr2 is a censorship resistant DID Method using the Bitcoin blockchain as a Verifiable Data Registry to announce changes to the DID document. This is the core method implementation for the did-btcr2-js monorepo.",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -66,49 +66,35 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@noble/curves": "^1.9.7",
|
|
68
68
|
"@noble/hashes": "^1.8.0",
|
|
69
|
-
"@noble/secp256k1": "^2.3.0",
|
|
70
69
|
"@scure/base": "^1.2.6",
|
|
71
|
-
"@scure/bip32": "^1.7.0",
|
|
72
|
-
"@scure/bip39": "^1.6.0",
|
|
73
70
|
"@scure/btc-signer": "^1.8.1",
|
|
74
|
-
"@web5/common": "^1.1.0",
|
|
75
|
-
"@web5/crypto": "^1.0.6",
|
|
76
71
|
"@web5/dids": "^1.2.0",
|
|
77
|
-
"canonicalize": "^2.1.0",
|
|
78
|
-
"dotenv": "^16.6.1",
|
|
79
|
-
"nostr-tools": "^2.23.3",
|
|
80
72
|
"@did-btcr2/bitcoin": "^0.8.0",
|
|
81
|
-
"@did-btcr2/
|
|
82
|
-
"@did-btcr2/cryptosuite": "^9.0.0",
|
|
73
|
+
"@did-btcr2/smt": "^0.3.0",
|
|
83
74
|
"@did-btcr2/keypair": "^0.13.1",
|
|
84
|
-
"@did-btcr2/
|
|
75
|
+
"@did-btcr2/common": "^9.1.0",
|
|
76
|
+
"@did-btcr2/cryptosuite": "^9.0.0"
|
|
85
77
|
},
|
|
86
78
|
"devDependencies": {
|
|
87
79
|
"@eslint/js": "^9.39.4",
|
|
88
80
|
"@types/chai": "^5.2.3",
|
|
89
|
-
"@types/chai-as-promised": "^8.0.2",
|
|
90
|
-
"@types/eslint": "^9.6.1",
|
|
91
81
|
"@types/mocha": "^10.0.10",
|
|
92
82
|
"@types/node": "^25.5.0",
|
|
93
83
|
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
94
84
|
"@typescript-eslint/parser": "^8.58.0",
|
|
95
85
|
"c8": "^10.1.3",
|
|
96
86
|
"chai": "^5.3.3",
|
|
97
|
-
"chai-as-promised": "^8.0.2",
|
|
98
87
|
"commander": "^13.1.0",
|
|
99
88
|
"esbuild": "^0.24.2",
|
|
100
89
|
"eslint": "^9.39.4",
|
|
101
90
|
"eslint-plugin-mocha": "^10.5.0",
|
|
102
91
|
"globals": "^15.15.0",
|
|
103
92
|
"mocha": "^10.8.2",
|
|
104
|
-
"mocha-junit-reporter": "^2.2.1",
|
|
105
93
|
"multiformats": "^13.4.2",
|
|
106
94
|
"node-stdlib-browser": "^1.3.1",
|
|
107
95
|
"rimraf": "^6.1.3",
|
|
108
96
|
"tsup": "^8.5.1",
|
|
109
|
-
"
|
|
110
|
-
"typescript": "^5.9.3",
|
|
111
|
-
"typescript-eslint": "^8.58.0"
|
|
97
|
+
"typescript": "^5.9.3"
|
|
112
98
|
},
|
|
113
99
|
"scripts": {
|
|
114
100
|
"clean": "rimraf dist coverage tests/compiled",
|
|
@@ -229,27 +229,97 @@ export function opReturnScript(signalBytes: Uint8Array): Uint8Array {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
232
|
+
* Minimum value (sats) a beacon UTXO must exceed for {@link selectSpendableUtxo} to
|
|
233
|
+
* treat it as spendable. This is a fixed, conservative, script-kind-agnostic floor:
|
|
234
|
+
* an output at or below it is too small to be worth spending, so selection discards
|
|
235
|
+
* it in favor of a larger confirmed UTXO. Keeping the floor a constant (rather than
|
|
236
|
+
* deriving it from a fee estimate) keeps selection pure and fee-estimator-independent.
|
|
237
|
+
*
|
|
238
|
+
* The floor is a coarse pre-filter, not the fee-coverage boundary: whether a selected
|
|
239
|
+
* UTXO actually covers the transaction fee is a separate check, enforced against the
|
|
240
|
+
* live {@link FeeEstimator} by the builders' `value <= feeSats` guard
|
|
241
|
+
* ({@link SinglePartyBeacon.buildSinglePartyTx} and {@link buildAggregationBeaconTx}).
|
|
242
|
+
* At the default 5 sat/vB rate that fee (roughly 775 to 1200 sats across the three
|
|
243
|
+
* script kinds) sits above this floor, so a UTXO can clear the dust filter and still
|
|
244
|
+
* be rejected as insufficient; conversely, at a very low fee rate an output near the
|
|
245
|
+
* floor could cover the fee. The floor's job is only to skip trivially small inputs.
|
|
246
|
+
*
|
|
247
|
+
* The value is the standard Bitcoin Core P2PKH dust threshold, the largest of the
|
|
248
|
+
* three singleton beacon script kinds (P2PKH 546, P2TR 330, P2WPKH 294 per
|
|
249
|
+
* {@link DUST_LIMIT_SATS}): a UTXO above it is non-dust under any beacon address kind.
|
|
250
|
+
* Distinct from {@link DUST_LIMIT_SATS}, which sizes the outgoing change output by
|
|
251
|
+
* kind; this bounds the incoming UTXO chosen to fund the transaction.
|
|
234
252
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
export const SPENDABLE_DUST_LIMIT_SATS = 546;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Deterministic ordering for spendable UTXO selection: deepest first (ascending
|
|
257
|
+
* block height, so the most-confirmed UTXO sorts first), tie-broken by `txid` then
|
|
258
|
+
* `vout`. The tie-break makes the winner independent of the order the REST API
|
|
259
|
+
* returns UTXOs in, so retries and independent resolvers converge on the same input.
|
|
260
|
+
*/
|
|
261
|
+
function byDepthThenId(a: AddressUtxo, b: AddressUtxo): number {
|
|
262
|
+
if(a.status.block_height !== b.status.block_height) {
|
|
263
|
+
return a.status.block_height - b.status.block_height;
|
|
264
|
+
}
|
|
265
|
+
const txidOrder = a.txid.localeCompare(b.txid);
|
|
266
|
+
return txidOrder !== 0 ? txidOrder : a.vout - b.vout;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Select the beacon UTXO to fund a signal transaction from the set of UTXOs at a
|
|
271
|
+
* beacon address. Pure and deterministic: the same address state always yields the
|
|
272
|
+
* same input, across broadcast retries and across independent resolvers.
|
|
273
|
+
*
|
|
274
|
+
* Filters to confirmed UTXOs (an unconfirmed input is reorg- and RBF-unsafe: a
|
|
275
|
+
* signal built on it can be orphaned or double-spent before it confirms), then drops
|
|
276
|
+
* dust at or below {@link SPENDABLE_DUST_LIMIT_SATS}, then picks the deepest via
|
|
277
|
+
* {@link byDepthThenId}. The did:btcr2 spec does not mandate a selection rule or a
|
|
278
|
+
* confirmation depth (only the security considerations favor deeper confirmations),
|
|
279
|
+
* so this is an implementation policy: prefer safety and reproducibility over
|
|
280
|
+
* spending the newest or largest output.
|
|
281
|
+
*
|
|
282
|
+
* @param utxos UTXOs reported at the beacon address.
|
|
283
|
+
* @param address Beacon address, used only to annotate thrown errors.
|
|
284
|
+
* @returns The confirmed, non-dust, deepest UTXO.
|
|
285
|
+
* @throws {BeaconError} `UNFUNDED_BEACON_ADDRESS` when no UTXOs exist at all;
|
|
286
|
+
* `NO_SPENDABLE_BEACON_UTXO` when UTXOs exist but none are both confirmed and above
|
|
287
|
+
* the dust limit (the message distinguishes all-unconfirmed from all-dust).
|
|
288
|
+
*/
|
|
289
|
+
export function selectSpendableUtxo(utxos: Array<AddressUtxo>, address?: string): AddressUtxo {
|
|
240
290
|
if(!utxos.length) {
|
|
241
291
|
throw new BeaconError(
|
|
242
292
|
'No UTXOs found, please fund address!',
|
|
243
|
-
'UNFUNDED_BEACON_ADDRESS', { address
|
|
293
|
+
'UNFUNDED_BEACON_ADDRESS', { address }
|
|
244
294
|
);
|
|
245
295
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
296
|
+
const confirmed = utxos.filter(utxo => utxo.status.confirmed === true);
|
|
297
|
+
const spendable = confirmed.filter(utxo => utxo.value > SPENDABLE_DUST_LIMIT_SATS);
|
|
298
|
+
if(!spendable.length) {
|
|
299
|
+
const reason = confirmed.length === 0
|
|
300
|
+
? `all ${utxos.length} UTXO(s) are unconfirmed`
|
|
301
|
+
: `all ${confirmed.length} confirmed UTXO(s) are at or below the ${SPENDABLE_DUST_LIMIT_SATS}-sat dust limit`;
|
|
248
302
|
throw new BeaconError(
|
|
249
|
-
|
|
250
|
-
'
|
|
303
|
+
`No spendable UTXO at beacon address: ${reason}.`,
|
|
304
|
+
'NO_SPENDABLE_BEACON_UTXO',
|
|
305
|
+
{ address, total: utxos.length, confirmed: confirmed.length, dustLimit: SPENDABLE_DUST_LIMIT_SATS }
|
|
251
306
|
);
|
|
252
307
|
}
|
|
308
|
+
return [ ...spendable ].sort(byDepthThenId)[0]!;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Fetch the deepest confirmed, non-dust spendable UTXO at `bitcoinAddress` plus the
|
|
313
|
+
* raw bytes of its parent transaction (needed by PSBT inputs). Selection is delegated
|
|
314
|
+
* to {@link selectSpendableUtxo}; throws {@link BeaconError} when the address is
|
|
315
|
+
* unfunded or has no confirmed, non-dust UTXO.
|
|
316
|
+
*/
|
|
317
|
+
async function fetchSpendableUtxo(
|
|
318
|
+
bitcoinAddress: string,
|
|
319
|
+
bitcoin: BitcoinConnection,
|
|
320
|
+
): Promise<{ utxo: AddressUtxo; prevTxBytes: Uint8Array }> {
|
|
321
|
+
const utxos = await bitcoin.rest.address.getUtxos(bitcoinAddress);
|
|
322
|
+
const utxo = selectSpendableUtxo(utxos, bitcoinAddress);
|
|
253
323
|
const prevTxHex = await bitcoin.rest.transaction.getHex(utxo.txid);
|
|
254
324
|
return { utxo, prevTxBytes: hexToBytes(prevTxHex) };
|
|
255
325
|
}
|
|
@@ -1,25 +1,106 @@
|
|
|
1
|
+
import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
|
|
2
|
+
import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
1
3
|
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
4
|
+
import type { DidDocument, DidVerificationMethod } from '../utils/did-document.js';
|
|
2
5
|
import { Identifier } from './identifier.js';
|
|
6
|
+
import { Resolver } from './resolver.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derive the aggregation communication public key from a resolved did:btcr2 DID Document.
|
|
10
|
+
*
|
|
11
|
+
* The communication key is the verification method referenced by
|
|
12
|
+
* `capabilityInvocation[0]`, resolved to its public key. This is the exact relationship
|
|
13
|
+
* the method already enforces for DID updates (construct and sign require the signing
|
|
14
|
+
* method to be in `capabilityInvocation`, the update proof is built and verified with
|
|
15
|
+
* `proofPurpose: 'capabilityInvocation'`), so binding the transport communication key to
|
|
16
|
+
* it yields the invariant "transport-authenticated as D implies authorized to update D."
|
|
17
|
+
* For a KEY (`k1`) document the single deterministic key is already the sole
|
|
18
|
+
* `capabilityInvocation` entry, so this is a no-op for KEY DIDs.
|
|
19
|
+
*
|
|
20
|
+
* There is deliberately no `verificationMethod[0]` fallback: a document without
|
|
21
|
+
* `capabilityInvocation` cannot be updated at all, so it is useless for aggregation and is
|
|
22
|
+
* rejected here rather than bound to an unrelated key.
|
|
23
|
+
*
|
|
24
|
+
* @param {DidDocument} document The resolved DID Document (placeholder id already replaced).
|
|
25
|
+
* @returns {CompressedSecp256k1PublicKey} The compressed public key of the communication method.
|
|
26
|
+
* @throws {DidDocumentError} If `capabilityInvocation` is absent or its first entry does not
|
|
27
|
+
* resolve to a verification method in the document.
|
|
28
|
+
*/
|
|
29
|
+
export function getAggregationCommunicationKey(
|
|
30
|
+
document: DidDocument,
|
|
31
|
+
): CompressedSecp256k1PublicKey {
|
|
32
|
+
const invocation = document.capabilityInvocation?.[0];
|
|
33
|
+
if(invocation === undefined) {
|
|
34
|
+
throw new DidDocumentError(
|
|
35
|
+
'Cannot derive aggregation communication key: capabilityInvocation is absent',
|
|
36
|
+
INVALID_DID_DOCUMENT, { id: document.id }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve capabilityInvocation[0] to a verification method: a string reference is
|
|
41
|
+
// dereferenced by id against verificationMethod; an embedded method is used directly.
|
|
42
|
+
// A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
|
|
43
|
+
// and does not resolve embedded methods.
|
|
44
|
+
const vm: DidVerificationMethod | undefined = typeof invocation === 'string'
|
|
45
|
+
? document.verificationMethod?.find(method => method.id === invocation)
|
|
46
|
+
: invocation;
|
|
47
|
+
|
|
48
|
+
if(!vm) {
|
|
49
|
+
throw new DidDocumentError(
|
|
50
|
+
`Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" `
|
|
51
|
+
+ 'does not resolve to a verification method',
|
|
52
|
+
INVALID_DID_DOCUMENT, { id: document.id, invocation }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return SchnorrMultikey.fromVerificationMethod(vm).publicKey;
|
|
57
|
+
}
|
|
3
58
|
|
|
4
59
|
/**
|
|
5
60
|
* Resolve a did:btcr2 sender's communication public key from its DID, for the
|
|
6
|
-
* aggregation HTTP transport's `resolveSenderPk` option
|
|
7
|
-
* its genesis public key. The aggregation transport is DID-method-agnostic and does
|
|
8
|
-
* not name `Identifier`; method supplies this resolver when it wires the transport so
|
|
9
|
-
* a sender that is not a pre-registered peer can still be authenticated from its DID.
|
|
61
|
+
* aggregation HTTP transport's `resolveSenderPk` option.
|
|
10
62
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
63
|
+
* A KEY (`k1`) identifier decodes directly to its genesis public key: the DID string is
|
|
64
|
+
* the key. An EXTERNAL (`x1`) identifier is a commitment to the hash of a genesis
|
|
65
|
+
* document, so there is no key in the DID string. When the genesis document is supplied
|
|
66
|
+
* in-band (via `opts.genesisDocument`), it is self-verifying against the DID: `Resolver`'s
|
|
67
|
+
* external path recomputes its canonical hash, compares it to the identifier's genesis
|
|
68
|
+
* bytes (throwing on mismatch), and resolves it, after which the communication key is
|
|
69
|
+
* derived from `capabilityInvocation[0]` ({@link getAggregationCommunicationKey}). Without
|
|
70
|
+
* a genesis document, an `x1` DID still resolves to `undefined`, so callers that pass no
|
|
71
|
+
* second argument behave exactly as before.
|
|
72
|
+
*
|
|
73
|
+
* The aggregation transport is DID-method-agnostic and does not name `Identifier`; method
|
|
74
|
+
* supplies this resolver when it wires the transport so a sender that is not a
|
|
75
|
+
* pre-registered peer can still be authenticated from its DID.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} did The sender's DID.
|
|
78
|
+
* @param {object} [opts] Optional resolution inputs.
|
|
79
|
+
* @param {object} [opts.genesisDocument] The `x1` sender's genesis document, carried in-band
|
|
80
|
+
* on the bootstrap opt-in. Ignored for KEY identifiers.
|
|
81
|
+
* @returns {CompressedSecp256k1PublicKey | undefined} The sender's compressed public key, or
|
|
82
|
+
* `undefined` when the DID is not a decodable did:btcr2 identifier, is an `x1` identifier
|
|
83
|
+
* with no (or a non-matching) genesis document, or has no usable communication key.
|
|
14
84
|
*/
|
|
15
|
-
export function resolveBtcr2SenderPk(
|
|
85
|
+
export function resolveBtcr2SenderPk(
|
|
86
|
+
did: string,
|
|
87
|
+
opts?: { genesisDocument?: object },
|
|
88
|
+
): CompressedSecp256k1PublicKey | undefined {
|
|
16
89
|
try {
|
|
17
90
|
const components = Identifier.decode(did);
|
|
18
91
|
if(components.idType === 'KEY') {
|
|
19
92
|
return new CompressedSecp256k1PublicKey(components.genesisBytes);
|
|
20
93
|
}
|
|
94
|
+
// EXTERNAL (x1): the DID commits to the hash of a genesis document. With the genesis
|
|
95
|
+
// supplied in-band, verify it hashes to the DID and derive the communication key from
|
|
96
|
+
// it; without it, there is no key to return.
|
|
97
|
+
if(opts?.genesisDocument) {
|
|
98
|
+
const document = Resolver.external(components, opts.genesisDocument);
|
|
99
|
+
return getAggregationCommunicationKey(document);
|
|
100
|
+
}
|
|
21
101
|
} catch {
|
|
22
|
-
// Not a decodable did:btcr2
|
|
102
|
+
// Not a decodable did:btcr2 identifier, the genesis does not hash to the DID, or the
|
|
103
|
+
// document has no usable capabilityInvocation communication key.
|
|
23
104
|
}
|
|
24
105
|
return undefined;
|
|
25
106
|
}
|