@did-btcr2/method 0.27.0 → 0.29.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 +38 -9
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +20181 -31588
- package/dist/browser.mjs +20110 -31517
- package/dist/cjs/index.js +1355 -422
- package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
- package/dist/esm/core/aggregation/cohort.js +31 -8
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/logger.js +15 -0
- package/dist/esm/core/aggregation/logger.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js +12 -1
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +90 -0
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/index.js +1 -0
- package/dist/esm/core/aggregation/messages/index.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +39 -46
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +34 -4
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +143 -15
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +44 -5
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
- package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
- package/dist/esm/core/aggregation/transport/nostr.js +245 -16
- package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
- package/dist/esm/core/beacon/beacon.js +147 -61
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/utils.js +14 -9
- package/dist/esm/core/beacon/utils.js.map +1 -1
- package/dist/esm/core/updater.js +269 -0
- package/dist/esm/core/updater.js.map +1 -0
- package/dist/esm/did-btcr2.js +30 -46
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/did-document.js +2 -2
- package/dist/esm/utils/did-document.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
- package/dist/types/core/aggregation/cohort.d.ts +20 -3
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/logger.d.ts +22 -0
- package/dist/types/core/aggregation/logger.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/base.d.ts +13 -1
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/index.d.ts +1 -0
- package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +2 -0
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +32 -6
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +8 -2
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +33 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +5 -1
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
- package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/transport.d.ts +25 -0
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +85 -18
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/utils.d.ts +2 -2
- package/dist/types/core/beacon/utils.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts +178 -0
- package/dist/types/core/updater.d.ts.map +1 -0
- package/dist/types/did-btcr2.d.ts +23 -23
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -6
- package/src/core/aggregation/beacon-strategy.ts +123 -0
- package/src/core/aggregation/cohort.ts +34 -8
- package/src/core/aggregation/logger.ts +33 -0
- package/src/core/aggregation/messages/base.ts +20 -5
- package/src/core/aggregation/messages/bodies.ts +223 -0
- package/src/core/aggregation/messages/factories.ts +1 -0
- package/src/core/aggregation/messages/index.ts +1 -0
- package/src/core/aggregation/participant.ts +40 -46
- package/src/core/aggregation/runner/events.ts +27 -3
- package/src/core/aggregation/runner/participant-runner.ts +42 -4
- package/src/core/aggregation/runner/service-runner.ts +227 -19
- package/src/core/aggregation/service.ts +189 -20
- package/src/core/aggregation/signing-session.ts +65 -7
- package/src/core/aggregation/transport/didcomm.ts +17 -0
- package/src/core/aggregation/transport/nostr.ts +266 -23
- package/src/core/aggregation/transport/transport.ts +33 -0
- package/src/core/beacon/beacon.ts +217 -76
- package/src/core/beacon/utils.ts +16 -11
- package/src/core/updater.ts +415 -0
- package/src/did-btcr2.ts +36 -71
- package/src/index.ts +4 -1
- package/src/utils/did-document.ts +2 -2
- package/dist/esm/core/update.js +0 -112
- package/dist/esm/core/update.js.map +0 -1
- package/dist/types/core/update.d.ts +0 -52
- package/dist/types/core/update.d.ts.map +0 -1
- package/src/core/update.ts +0 -158
package/dist/cjs/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AGGREGATED_NONCE: () => AGGREGATED_NONCE,
|
|
34
34
|
AGGREGATION_MESSAGE_PREFIX: () => AGGREGATION_MESSAGE_PREFIX,
|
|
35
|
+
AGGREGATION_WIRE_VERSION: () => AGGREGATION_WIRE_VERSION,
|
|
35
36
|
AUTHORIZATION_REQUEST: () => AUTHORIZATION_REQUEST,
|
|
36
37
|
AggregateBeaconError: () => AggregateBeaconError,
|
|
37
38
|
AggregationCohort: () => AggregationCohort,
|
|
@@ -58,6 +59,10 @@ __export(index_exports, {
|
|
|
58
59
|
COHORT_OPT_IN: () => COHORT_OPT_IN,
|
|
59
60
|
COHORT_OPT_IN_ACCEPT: () => COHORT_OPT_IN_ACCEPT,
|
|
60
61
|
COHORT_READY: () => COHORT_READY,
|
|
62
|
+
CONSOLE_LOGGER: () => CONSOLE_LOGGER,
|
|
63
|
+
DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
64
|
+
DEFAULT_BROADCAST_LOOKBACK_MS: () => DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
65
|
+
DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
61
66
|
DEFAULT_NOSTR_RELAYS: () => DEFAULT_NOSTR_RELAYS,
|
|
62
67
|
DID_REGEX: () => DID_REGEX,
|
|
63
68
|
DISTRIBUTE_AGGREGATED_DATA: () => DISTRIBUTE_AGGREGATED_DATA,
|
|
@@ -75,6 +80,7 @@ __export(index_exports, {
|
|
|
75
80
|
ParticipantCohortPhase: () => ParticipantCohortPhase,
|
|
76
81
|
Resolver: () => Resolver,
|
|
77
82
|
SIGNATURE_AUTHORIZATION: () => SIGNATURE_AUTHORIZATION,
|
|
83
|
+
SILENT_LOGGER: () => SILENT_LOGGER,
|
|
78
84
|
SMTBeacon: () => SMTBeacon,
|
|
79
85
|
SMTBeaconError: () => SMTBeaconError,
|
|
80
86
|
SUBMIT_UPDATE: () => SUBMIT_UPDATE,
|
|
@@ -83,12 +89,14 @@ __export(index_exports, {
|
|
|
83
89
|
SigningSessionPhase: () => SigningSessionPhase,
|
|
84
90
|
SingletonBeacon: () => SingletonBeacon,
|
|
85
91
|
SingletonBeaconError: () => SingletonBeaconError,
|
|
92
|
+
StaticFeeEstimator: () => StaticFeeEstimator,
|
|
86
93
|
TransportAdapterError: () => TransportAdapterError,
|
|
87
94
|
TransportError: () => TransportError,
|
|
88
95
|
TransportFactory: () => TransportFactory,
|
|
89
96
|
TypedEventEmitter: () => TypedEventEmitter,
|
|
90
|
-
|
|
97
|
+
Updater: () => Updater,
|
|
91
98
|
VALIDATION_ACK: () => VALIDATION_ACK,
|
|
99
|
+
buildAggregationBeaconTx: () => buildAggregationBeaconTx,
|
|
92
100
|
createAggregatedNonceMessage: () => createAggregatedNonceMessage,
|
|
93
101
|
createAuthorizationRequestMessage: () => createAuthorizationRequestMessage,
|
|
94
102
|
createCohortAdvertMessage: () => createCohortAdvertMessage,
|
|
@@ -100,41 +108,114 @@ __export(index_exports, {
|
|
|
100
108
|
createSignatureAuthorizationMessage: () => createSignatureAuthorizationMessage,
|
|
101
109
|
createSubmitUpdateMessage: () => createSubmitUpdateMessage,
|
|
102
110
|
createValidationAckMessage: () => createValidationAckMessage,
|
|
111
|
+
getBeaconStrategy: () => getBeaconStrategy,
|
|
112
|
+
isAggregatedNonceMessage: () => isAggregatedNonceMessage,
|
|
103
113
|
isAggregationMessageType: () => isAggregationMessageType,
|
|
114
|
+
isAuthorizationRequestMessage: () => isAuthorizationRequestMessage,
|
|
115
|
+
isCohortAdvertMessage: () => isCohortAdvertMessage,
|
|
116
|
+
isCohortOptInAcceptMessage: () => isCohortOptInAcceptMessage,
|
|
117
|
+
isCohortOptInMessage: () => isCohortOptInMessage,
|
|
118
|
+
isCohortReadyMessage: () => isCohortReadyMessage,
|
|
119
|
+
isDistributeAggregatedDataMessage: () => isDistributeAggregatedDataMessage,
|
|
104
120
|
isKeygenMessageType: () => isKeygenMessageType,
|
|
121
|
+
isNonceContributionMessage: () => isNonceContributionMessage,
|
|
105
122
|
isSignMessageType: () => isSignMessageType,
|
|
106
|
-
|
|
123
|
+
isSignatureAuthorizationMessage: () => isSignatureAuthorizationMessage,
|
|
124
|
+
isSubmitUpdateMessage: () => isSubmitUpdateMessage,
|
|
125
|
+
isUpdateMessageType: () => isUpdateMessageType,
|
|
126
|
+
isValidationAckMessage: () => isValidationAckMessage,
|
|
127
|
+
opReturnScript: () => opReturnScript,
|
|
128
|
+
registerBeaconStrategy: () => registerBeaconStrategy
|
|
107
129
|
});
|
|
108
130
|
module.exports = __toCommonJS(index_exports);
|
|
109
131
|
|
|
110
132
|
// src/core/aggregation/service.ts
|
|
133
|
+
var import_common4 = require("@did-btcr2/common");
|
|
134
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
111
135
|
var import_utils2 = require("@noble/hashes/utils");
|
|
112
136
|
|
|
113
|
-
// src/core/aggregation/
|
|
114
|
-
var
|
|
137
|
+
// src/core/aggregation/beacon-strategy.ts
|
|
138
|
+
var import_common = require("@did-btcr2/common");
|
|
115
139
|
var import_smt = require("@did-btcr2/smt");
|
|
140
|
+
var CAS_STRATEGY = {
|
|
141
|
+
type: "CASBeacon",
|
|
142
|
+
buildAggregatedData(cohort) {
|
|
143
|
+
cohort.buildCASAnnouncement();
|
|
144
|
+
},
|
|
145
|
+
getDistributePayload(cohort) {
|
|
146
|
+
return { casAnnouncement: cohort.casAnnouncement };
|
|
147
|
+
},
|
|
148
|
+
validateParticipantView({ participantDid, expectedHash, body }) {
|
|
149
|
+
const casAnnouncement = body.casAnnouncement;
|
|
150
|
+
if (!casAnnouncement) return { matches: false };
|
|
151
|
+
return {
|
|
152
|
+
matches: casAnnouncement[participantDid] === expectedHash,
|
|
153
|
+
casAnnouncement
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var SMT_STRATEGY = {
|
|
158
|
+
type: "SMTBeacon",
|
|
159
|
+
buildAggregatedData(cohort) {
|
|
160
|
+
cohort.buildSMTTree();
|
|
161
|
+
},
|
|
162
|
+
getDistributePayload(cohort, participantDid) {
|
|
163
|
+
const proof = cohort.smtProofs?.get(participantDid);
|
|
164
|
+
return { smtProof: proof };
|
|
165
|
+
},
|
|
166
|
+
validateParticipantView({ participantDid, submittedUpdate, body }) {
|
|
167
|
+
const smtProof = body.smtProof;
|
|
168
|
+
if (!smtProof?.updateId || !smtProof?.nonce) return { matches: false };
|
|
169
|
+
const canonicalBytes = new TextEncoder().encode((0, import_common.canonicalize)(submittedUpdate));
|
|
170
|
+
const expectedUpdateId = (0, import_smt.hashToHex)((0, import_smt.blockHash)(canonicalBytes));
|
|
171
|
+
if (smtProof.updateId !== expectedUpdateId) {
|
|
172
|
+
return { matches: false, smtProof };
|
|
173
|
+
}
|
|
174
|
+
const index = (0, import_smt.didToIndex)(participantDid);
|
|
175
|
+
const candidateHash = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.hexToHash)(smtProof.nonce)), (0, import_smt.hexToHash)(smtProof.updateId));
|
|
176
|
+
return {
|
|
177
|
+
matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash),
|
|
178
|
+
smtProof
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var STRATEGIES = /* @__PURE__ */ new Map([
|
|
183
|
+
[CAS_STRATEGY.type, CAS_STRATEGY],
|
|
184
|
+
[SMT_STRATEGY.type, SMT_STRATEGY]
|
|
185
|
+
]);
|
|
186
|
+
function registerBeaconStrategy(strategy) {
|
|
187
|
+
STRATEGIES.set(strategy.type, strategy);
|
|
188
|
+
}
|
|
189
|
+
function getBeaconStrategy(type) {
|
|
190
|
+
return STRATEGIES.get(type);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/core/aggregation/cohort.ts
|
|
194
|
+
var import_common3 = require("@did-btcr2/common");
|
|
195
|
+
var import_smt2 = require("@did-btcr2/smt");
|
|
196
|
+
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
116
197
|
var import_utils = require("@noble/hashes/utils");
|
|
198
|
+
var import_btc_signer = require("@scure/btc-signer");
|
|
117
199
|
var import_musig2 = require("@scure/btc-signer/musig2");
|
|
118
|
-
var import_bitcoinjs_lib = require("bitcoinjs-lib");
|
|
119
200
|
|
|
120
201
|
// src/core/aggregation/errors.ts
|
|
121
|
-
var
|
|
122
|
-
var AggregationServiceError = class extends
|
|
202
|
+
var import_common2 = require("@did-btcr2/common");
|
|
203
|
+
var AggregationServiceError = class extends import_common2.MethodError {
|
|
123
204
|
constructor(message, type = "AggregationServiceError", data) {
|
|
124
205
|
super(message, type, data);
|
|
125
206
|
}
|
|
126
207
|
};
|
|
127
|
-
var AggregationParticipantError = class extends
|
|
208
|
+
var AggregationParticipantError = class extends import_common2.MethodError {
|
|
128
209
|
constructor(message, type = "AggregationParticipantError", data) {
|
|
129
210
|
super(message, type, data);
|
|
130
211
|
}
|
|
131
212
|
};
|
|
132
|
-
var AggregationCohortError = class extends
|
|
213
|
+
var AggregationCohortError = class extends import_common2.MethodError {
|
|
133
214
|
constructor(message, type = "AggregationCohortError", data) {
|
|
134
215
|
super(message, type, data);
|
|
135
216
|
}
|
|
136
217
|
};
|
|
137
|
-
var SigningSessionError = class extends
|
|
218
|
+
var SigningSessionError = class extends import_common2.MethodError {
|
|
138
219
|
constructor(message, type = "SigningSessionError", data) {
|
|
139
220
|
super(message, type, data);
|
|
140
221
|
}
|
|
@@ -154,10 +235,21 @@ var AggregationCohort = class {
|
|
|
154
235
|
beaconType;
|
|
155
236
|
/** List of participant DIDs that have been accepted into the cohort. */
|
|
156
237
|
participants = [];
|
|
238
|
+
/**
|
|
239
|
+
* Mapping from participant DID → their compressed secp256k1 public key.
|
|
240
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327) — this lets
|
|
241
|
+
* callers look up a participant's key without knowing their position in the
|
|
242
|
+
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
243
|
+
*/
|
|
244
|
+
participantKeys = /* @__PURE__ */ new Map();
|
|
157
245
|
/** Sorted list of cohort participants' compressed public keys. */
|
|
158
246
|
#cohortKeys = [];
|
|
159
|
-
/**
|
|
160
|
-
|
|
247
|
+
/**
|
|
248
|
+
* BIP-341 TapTweak — `taggedHash("TapTweak", internalPubkey)` for a key-path-only
|
|
249
|
+
* Taproot output. Despite prior naming, this is NOT a Merkle root: key-path-only
|
|
250
|
+
* spends have no script tree.
|
|
251
|
+
*/
|
|
252
|
+
tapTweak = new Uint8Array();
|
|
161
253
|
/** The n-of-n MuSig2 Taproot beacon address. */
|
|
162
254
|
beaconAddress = "";
|
|
163
255
|
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
@@ -188,7 +280,7 @@ var AggregationCohort = class {
|
|
|
188
280
|
}
|
|
189
281
|
/**
|
|
190
282
|
* Computes the n-of-n MuSig2 Taproot beacon address from cohort keys.
|
|
191
|
-
* Sets `
|
|
283
|
+
* Sets `tapTweak` to the BIP-341 key-path-only tweak.
|
|
192
284
|
*/
|
|
193
285
|
computeBeaconAddress() {
|
|
194
286
|
if (this.#cohortKeys.length === 0) {
|
|
@@ -200,8 +292,8 @@ var AggregationCohort = class {
|
|
|
200
292
|
}
|
|
201
293
|
const keyAggContext = (0, import_musig2.keyAggregate)(this.#cohortKeys);
|
|
202
294
|
const aggPubkey = (0, import_musig2.keyAggExport)(keyAggContext);
|
|
203
|
-
const payment =
|
|
204
|
-
this.
|
|
295
|
+
const payment = (0, import_btc_signer.p2tr)(aggPubkey);
|
|
296
|
+
this.tapTweak = import_secp256k1.schnorr.utils.taggedHash("TapTweak", aggPubkey);
|
|
205
297
|
if (!payment.address) {
|
|
206
298
|
throw new AggregationCohortError(
|
|
207
299
|
"Failed to compute Taproot address",
|
|
@@ -235,6 +327,18 @@ var AggregationCohort = class {
|
|
|
235
327
|
);
|
|
236
328
|
}
|
|
237
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Returns the position of a participant's public key in the sorted
|
|
332
|
+
* {@link cohortKeys} array, or -1 if the participant is not in the cohort.
|
|
333
|
+
* Required by MuSig2 partial-sig verification which indexes by signer position.
|
|
334
|
+
*/
|
|
335
|
+
indexOfParticipant(did) {
|
|
336
|
+
const pk = this.participantKeys.get(did);
|
|
337
|
+
if (!pk) return -1;
|
|
338
|
+
return this.#cohortKeys.findIndex(
|
|
339
|
+
(k) => k.length === pk.length && k.every((b, i) => b === pk[i])
|
|
340
|
+
);
|
|
341
|
+
}
|
|
238
342
|
addUpdate(participantDid, signedUpdate) {
|
|
239
343
|
if (!this.participants.includes(participantDid)) {
|
|
240
344
|
throw new AggregationCohortError(
|
|
@@ -263,10 +367,10 @@ var AggregationCohort = class {
|
|
|
263
367
|
}
|
|
264
368
|
const announcement = {};
|
|
265
369
|
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
266
|
-
announcement[did] = (0,
|
|
370
|
+
announcement[did] = (0, import_common3.canonicalHash)(signedUpdate);
|
|
267
371
|
}
|
|
268
372
|
this.casAnnouncement = announcement;
|
|
269
|
-
this.signalBytes = (0,
|
|
373
|
+
this.signalBytes = (0, import_common3.hash)((0, import_common3.canonicalize)(announcement));
|
|
270
374
|
return announcement;
|
|
271
375
|
}
|
|
272
376
|
/**
|
|
@@ -282,11 +386,11 @@ var AggregationCohort = class {
|
|
|
282
386
|
{ cohortId: this.id }
|
|
283
387
|
);
|
|
284
388
|
}
|
|
285
|
-
const tree = new
|
|
389
|
+
const tree = new import_smt2.BTCR2MerkleTree();
|
|
286
390
|
const entries = [];
|
|
287
391
|
const encoder = new TextEncoder();
|
|
288
392
|
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
289
|
-
const canonicalBytes = encoder.encode((0,
|
|
393
|
+
const canonicalBytes = encoder.encode((0, import_common3.canonicalize)(signedUpdate));
|
|
290
394
|
const nonce = (0, import_utils.randomBytes)(32);
|
|
291
395
|
entries.push({ did, nonce, signedUpdate: canonicalBytes });
|
|
292
396
|
}
|
|
@@ -328,28 +432,17 @@ var AggregationCohort = class {
|
|
|
328
432
|
}
|
|
329
433
|
};
|
|
330
434
|
|
|
331
|
-
// src/core/aggregation/messages/constants.ts
|
|
332
|
-
var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
|
|
333
|
-
var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
|
|
334
|
-
var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
335
|
-
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
336
|
-
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
337
|
-
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
338
|
-
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
339
|
-
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
340
|
-
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
341
|
-
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
342
|
-
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
343
|
-
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
344
|
-
|
|
345
435
|
// src/core/aggregation/messages/base.ts
|
|
436
|
+
var AGGREGATION_WIRE_VERSION = 1;
|
|
346
437
|
var BaseMessage = class {
|
|
347
438
|
type;
|
|
439
|
+
version;
|
|
348
440
|
to;
|
|
349
441
|
from;
|
|
350
442
|
body;
|
|
351
|
-
constructor({ type, to, from: from2, body }) {
|
|
443
|
+
constructor({ type, version, to, from: from2, body }) {
|
|
352
444
|
this.type = type;
|
|
445
|
+
this.version = version ?? AGGREGATION_WIRE_VERSION;
|
|
353
446
|
this.to = to;
|
|
354
447
|
this.from = from2;
|
|
355
448
|
this.body = body;
|
|
@@ -361,6 +454,7 @@ var BaseMessage = class {
|
|
|
361
454
|
toJSON() {
|
|
362
455
|
return {
|
|
363
456
|
type: this.type,
|
|
457
|
+
version: this.version,
|
|
364
458
|
to: this.to,
|
|
365
459
|
from: this.from,
|
|
366
460
|
body: this.body
|
|
@@ -368,6 +462,20 @@ var BaseMessage = class {
|
|
|
368
462
|
}
|
|
369
463
|
};
|
|
370
464
|
|
|
465
|
+
// src/core/aggregation/messages/constants.ts
|
|
466
|
+
var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
|
|
467
|
+
var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
|
|
468
|
+
var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
469
|
+
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
470
|
+
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
471
|
+
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
472
|
+
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
473
|
+
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
474
|
+
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
475
|
+
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
476
|
+
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
477
|
+
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
478
|
+
|
|
371
479
|
// src/core/aggregation/messages/factories.ts
|
|
372
480
|
function createCohortAdvertMessage(fields) {
|
|
373
481
|
const { from: from2, ...body } = fields;
|
|
@@ -455,8 +563,8 @@ var SigningSessionPhase = /* @__PURE__ */ ((SigningSessionPhase2) => {
|
|
|
455
563
|
})(SigningSessionPhase || {});
|
|
456
564
|
|
|
457
565
|
// src/core/aggregation/signing-session.ts
|
|
566
|
+
var import_btc_signer2 = require("@scure/btc-signer");
|
|
458
567
|
var musig2 = __toESM(require("@scure/btc-signer/musig2"), 1);
|
|
459
|
-
var import_bitcoinjs_lib2 = require("bitcoinjs-lib");
|
|
460
568
|
var BeaconSigningSession = class {
|
|
461
569
|
/** Unique identifier for this signing session. */
|
|
462
570
|
id;
|
|
@@ -498,11 +606,11 @@ var BeaconSigningSession = class {
|
|
|
498
606
|
"SIGHASH_ERROR"
|
|
499
607
|
);
|
|
500
608
|
}
|
|
501
|
-
return this.pendingTx.
|
|
609
|
+
return this.pendingTx.preimageWitnessV1(
|
|
502
610
|
0,
|
|
503
611
|
this.prevOutScripts,
|
|
504
|
-
|
|
505
|
-
|
|
612
|
+
import_btc_signer2.SigHash.DEFAULT,
|
|
613
|
+
this.prevOutValues
|
|
506
614
|
);
|
|
507
615
|
}
|
|
508
616
|
addNonceContribution(participantDid, nonceContribution) {
|
|
@@ -513,6 +621,13 @@ var BeaconSigningSession = class {
|
|
|
513
621
|
{ phase: this.phase }
|
|
514
622
|
);
|
|
515
623
|
}
|
|
624
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
625
|
+
throw new SigningSessionError(
|
|
626
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
627
|
+
"UNKNOWN_PARTICIPANT",
|
|
628
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
629
|
+
);
|
|
630
|
+
}
|
|
516
631
|
if (nonceContribution.length !== 66) {
|
|
517
632
|
throw new SigningSessionError(
|
|
518
633
|
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
@@ -548,6 +663,13 @@ var BeaconSigningSession = class {
|
|
|
548
663
|
"INVALID_PHASE"
|
|
549
664
|
);
|
|
550
665
|
}
|
|
666
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
667
|
+
throw new SigningSessionError(
|
|
668
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
669
|
+
"UNKNOWN_PARTICIPANT",
|
|
670
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
671
|
+
);
|
|
672
|
+
}
|
|
551
673
|
if (this.partialSignatures.has(participantDid)) {
|
|
552
674
|
throw new SigningSessionError(
|
|
553
675
|
`Duplicate partial signature from ${participantDid}.`,
|
|
@@ -573,9 +695,39 @@ var BeaconSigningSession = class {
|
|
|
573
695
|
this.aggregatedNonce,
|
|
574
696
|
this.cohort.cohortKeys,
|
|
575
697
|
this.sigHash,
|
|
576
|
-
[this.cohort.
|
|
698
|
+
[this.cohort.tapTweak],
|
|
577
699
|
[true]
|
|
578
700
|
);
|
|
701
|
+
const pubNoncesByIndex = new Array(this.cohort.cohortKeys.length);
|
|
702
|
+
for (const [did, nonce] of this.nonceContributions) {
|
|
703
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
704
|
+
if (idx < 0) {
|
|
705
|
+
throw new SigningSessionError(
|
|
706
|
+
`Cannot verify nonce from ${did}: participant key missing from cohort.`,
|
|
707
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
708
|
+
{ participantDid: did }
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
pubNoncesByIndex[idx] = nonce;
|
|
712
|
+
}
|
|
713
|
+
for (const [did, partialSig] of this.partialSignatures) {
|
|
714
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
715
|
+
if (idx < 0) {
|
|
716
|
+
throw new SigningSessionError(
|
|
717
|
+
`Cannot verify partial signature from ${did}: participant key missing from cohort.`,
|
|
718
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
719
|
+
{ participantDid: did }
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
|
|
723
|
+
if (!ok) {
|
|
724
|
+
throw new SigningSessionError(
|
|
725
|
+
`Bad partial signature from ${did}.`,
|
|
726
|
+
"BAD_PARTIAL_SIG",
|
|
727
|
+
{ participantDid: did, index: idx }
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
579
731
|
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
580
732
|
this.phase = "Complete" /* Complete */;
|
|
581
733
|
return this.signature;
|
|
@@ -593,6 +745,10 @@ var BeaconSigningSession = class {
|
|
|
593
745
|
/**
|
|
594
746
|
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
595
747
|
* Requires the aggregated nonce to have been set first (via the service).
|
|
748
|
+
*
|
|
749
|
+
* Zeros the stored `secretNonce` after use. JS cannot truly erase memory (GC
|
|
750
|
+
* and immutable strings), but overwriting the bytes shortens the exposure
|
|
751
|
+
* window and prevents accidental reuse or serialization of a spent nonce.
|
|
596
752
|
*/
|
|
597
753
|
generatePartialSignature(participantSecretKey) {
|
|
598
754
|
if (!this.aggregatedNonce) {
|
|
@@ -605,10 +761,13 @@ var BeaconSigningSession = class {
|
|
|
605
761
|
this.aggregatedNonce,
|
|
606
762
|
this.cohort.cohortKeys,
|
|
607
763
|
this.sigHash,
|
|
608
|
-
[this.cohort.
|
|
764
|
+
[this.cohort.tapTweak],
|
|
609
765
|
[true]
|
|
610
766
|
);
|
|
611
|
-
|
|
767
|
+
const partialSig = session.sign(this.secretNonce, participantSecretKey);
|
|
768
|
+
this.secretNonce.fill(0);
|
|
769
|
+
this.secretNonce = void 0;
|
|
770
|
+
return partialSig;
|
|
612
771
|
}
|
|
613
772
|
isComplete() {
|
|
614
773
|
return this.phase === "Complete" /* Complete */;
|
|
@@ -619,16 +778,32 @@ var BeaconSigningSession = class {
|
|
|
619
778
|
};
|
|
620
779
|
|
|
621
780
|
// src/core/aggregation/service.ts
|
|
781
|
+
var DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
|
|
622
782
|
var AggregationService = class {
|
|
623
783
|
did;
|
|
624
784
|
keys;
|
|
785
|
+
maxUpdateSizeBytes;
|
|
625
786
|
/** Per-cohort state, keyed by cohortId. */
|
|
626
787
|
#cohortStates = /* @__PURE__ */ new Map();
|
|
627
|
-
constructor({ did, keys }) {
|
|
788
|
+
constructor({ did, keys, maxUpdateSizeBytes }) {
|
|
628
789
|
this.did = did;
|
|
629
790
|
this.keys = keys;
|
|
791
|
+
this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
|
|
630
792
|
}
|
|
631
793
|
receive(message) {
|
|
794
|
+
const version = message.version;
|
|
795
|
+
if (version === void 0 || version !== AGGREGATION_WIRE_VERSION) {
|
|
796
|
+
const cohortId = message.body?.cohortId;
|
|
797
|
+
const state = cohortId ? this.#cohortStates.get(cohortId) : void 0;
|
|
798
|
+
if (state) {
|
|
799
|
+
state.rejections.push({
|
|
800
|
+
from: message.from,
|
|
801
|
+
code: "WRONG_VERSION",
|
|
802
|
+
reason: `Expected wire version ${AGGREGATION_WIRE_VERSION}, got ${String(version)}`
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
632
807
|
const type = message.type;
|
|
633
808
|
switch (type) {
|
|
634
809
|
case COHORT_OPT_IN:
|
|
@@ -650,6 +825,18 @@ var AggregationService = class {
|
|
|
650
825
|
break;
|
|
651
826
|
}
|
|
652
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Drain the rejection log for a cohort. Used by runners to surface silent
|
|
830
|
+
* drops (bad proof, oversized update, wrong version, etc.) as structured
|
|
831
|
+
* events without breaking the sans-I/O state machine contract.
|
|
832
|
+
*/
|
|
833
|
+
drainRejections(cohortId) {
|
|
834
|
+
const state = this.#cohortStates.get(cohortId);
|
|
835
|
+
if (!state) return [];
|
|
836
|
+
const out = state.rejections;
|
|
837
|
+
state.rejections = [];
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
653
840
|
/**
|
|
654
841
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
655
842
|
* Cohort starts in `Created` phase — call `advertise()` to broadcast.
|
|
@@ -666,7 +853,8 @@ var AggregationService = class {
|
|
|
666
853
|
cohort,
|
|
667
854
|
config,
|
|
668
855
|
pendingOptIns: /* @__PURE__ */ new Map(),
|
|
669
|
-
acceptedParticipants: /* @__PURE__ */ new Set()
|
|
856
|
+
acceptedParticipants: /* @__PURE__ */ new Set(),
|
|
857
|
+
rejections: []
|
|
670
858
|
});
|
|
671
859
|
return cohort.id;
|
|
672
860
|
}
|
|
@@ -719,6 +907,7 @@ var AggregationService = class {
|
|
|
719
907
|
const participantPk = message.body?.participantPk;
|
|
720
908
|
const communicationPk = message.body?.communicationPk;
|
|
721
909
|
if (!participantPk || !communicationPk) return;
|
|
910
|
+
if (state.acceptedParticipants.has(participantDid)) return;
|
|
722
911
|
state.pendingOptIns.set(participantDid, {
|
|
723
912
|
cohortId,
|
|
724
913
|
participantDid,
|
|
@@ -752,6 +941,7 @@ var AggregationService = class {
|
|
|
752
941
|
}
|
|
753
942
|
state.acceptedParticipants.add(participantDid);
|
|
754
943
|
state.cohort.participants.push(participantDid);
|
|
944
|
+
state.cohort.participantKeys.set(participantDid, optIn.participantPk);
|
|
755
945
|
state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
|
|
756
946
|
return [createCohortOptInAcceptMessage({
|
|
757
947
|
from: this.did,
|
|
@@ -802,6 +992,13 @@ var AggregationService = class {
|
|
|
802
992
|
if (!state) return /* @__PURE__ */ new Map();
|
|
803
993
|
return state.cohort.pendingUpdates;
|
|
804
994
|
}
|
|
995
|
+
/**
|
|
996
|
+
* Handle an incoming SUBMIT_UPDATE message from a participant containing their signed update to
|
|
997
|
+
* submit for aggregation.
|
|
998
|
+
* @param {BaseMessage} message - incoming SUBMIT_UPDATE message containing a participant's signed
|
|
999
|
+
* update to submit for aggregation
|
|
1000
|
+
* @returns {void} - no return value; updates the service state with the submitted update if valid
|
|
1001
|
+
*/
|
|
805
1002
|
#handleSubmitUpdate(message) {
|
|
806
1003
|
const cohortId = message.body?.cohortId;
|
|
807
1004
|
if (!cohortId) return;
|
|
@@ -809,7 +1006,31 @@ var AggregationService = class {
|
|
|
809
1006
|
if (!state) return;
|
|
810
1007
|
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
811
1008
|
const signedUpdate = message.body?.signedUpdate;
|
|
812
|
-
if (!signedUpdate)
|
|
1009
|
+
if (!signedUpdate) {
|
|
1010
|
+
state.rejections.push({
|
|
1011
|
+
from: message.from,
|
|
1012
|
+
code: "UPDATE_MALFORMED",
|
|
1013
|
+
reason: "SUBMIT_UPDATE missing signedUpdate body"
|
|
1014
|
+
});
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const canonicalSize = (0, import_common4.canonicalize)(signedUpdate).length;
|
|
1018
|
+
if (canonicalSize > this.maxUpdateSizeBytes) {
|
|
1019
|
+
state.rejections.push({
|
|
1020
|
+
from: message.from,
|
|
1021
|
+
code: "UPDATE_TOO_LARGE",
|
|
1022
|
+
reason: `Canonicalized update is ${canonicalSize} bytes; max allowed is ${this.maxUpdateSizeBytes}`
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (!this.#verifySubmittedUpdate(state, message.from, signedUpdate)) {
|
|
1027
|
+
state.rejections.push({
|
|
1028
|
+
from: message.from,
|
|
1029
|
+
code: "UPDATE_VERIFICATION_FAILED",
|
|
1030
|
+
reason: "BIP-340 Data Integrity proof verification failed"
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
813
1034
|
state.cohort.addUpdate(message.from, signedUpdate);
|
|
814
1035
|
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
815
1036
|
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
@@ -818,6 +1039,36 @@ var AggregationService = class {
|
|
|
818
1039
|
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
819
1040
|
}
|
|
820
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Verify the BIP-340 Schnorr Data Integrity proof on a submitted update using the
|
|
1044
|
+
* participant's public key from their cohort opt-in. Returns `false` (and the
|
|
1045
|
+
* update is silently dropped) if the proof is missing, the verificationMethod does
|
|
1046
|
+
* not name the sender's DID, the participant has no opt-in on record, or the
|
|
1047
|
+
* signature fails verification.
|
|
1048
|
+
* @param {ServiceCohortState} state - the current state of the cohort to which the update was submitted
|
|
1049
|
+
* @param {string} sender - the DID of the participant who submitted the update
|
|
1050
|
+
* @param {SignedBTCR2Update} signedUpdate - the signed update containing the proof to verify
|
|
1051
|
+
* @returns {boolean} - `true` if the proof is valid and the update can be accepted; `false` otherwise
|
|
1052
|
+
*/
|
|
1053
|
+
#verifySubmittedUpdate(state, sender, signedUpdate) {
|
|
1054
|
+
const proof = signedUpdate.proof;
|
|
1055
|
+
if (!proof?.verificationMethod || !proof.proofValue) return false;
|
|
1056
|
+
const vmDid = proof.verificationMethod.split("#")[0];
|
|
1057
|
+
if (vmDid !== sender) return false;
|
|
1058
|
+
const optIn = state.pendingOptIns.get(sender);
|
|
1059
|
+
if (!optIn) return false;
|
|
1060
|
+
try {
|
|
1061
|
+
const multikey = import_cryptosuite.SchnorrMultikey.fromPublicKey({
|
|
1062
|
+
id: proof.verificationMethod,
|
|
1063
|
+
controller: sender,
|
|
1064
|
+
publicKeyBytes: optIn.participantPk
|
|
1065
|
+
});
|
|
1066
|
+
const suite = new import_cryptosuite.BIP340Cryptosuite(multikey);
|
|
1067
|
+
return suite.verifyProof(signedUpdate).verified === true;
|
|
1068
|
+
} catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
821
1072
|
/**
|
|
822
1073
|
* Build the aggregated data structure (CAS Announcement or SMT tree) and
|
|
823
1074
|
* return distribute messages to send to all participants for validation.
|
|
@@ -834,29 +1085,28 @@ var AggregationService = class {
|
|
|
834
1085
|
{ cohortId, phase: state.phase }
|
|
835
1086
|
);
|
|
836
1087
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
} else if (state.config.beaconType === "SMTBeacon") {
|
|
840
|
-
state.cohort.buildSMTTree();
|
|
841
|
-
} else {
|
|
1088
|
+
const strategy = getBeaconStrategy(state.config.beaconType);
|
|
1089
|
+
if (!strategy) {
|
|
842
1090
|
throw new AggregationServiceError(
|
|
843
1091
|
`Unsupported beacon type: ${state.config.beaconType}`,
|
|
844
1092
|
"UNSUPPORTED_BEACON_TYPE",
|
|
845
1093
|
{ cohortId, beaconType: state.config.beaconType }
|
|
846
1094
|
);
|
|
847
1095
|
}
|
|
1096
|
+
strategy.buildAggregatedData(state.cohort);
|
|
848
1097
|
const signalBytesHex = (0, import_utils2.bytesToHex)(state.cohort.signalBytes);
|
|
849
1098
|
state.phase = "DataDistributed" /* DataDistributed */;
|
|
850
1099
|
const messages = [];
|
|
851
1100
|
for (const participantDid of state.cohort.participants) {
|
|
1101
|
+
const payload = strategy.getDistributePayload(state.cohort, participantDid);
|
|
852
1102
|
messages.push(createDistributeAggregatedDataMessage({
|
|
853
1103
|
from: this.did,
|
|
854
1104
|
to: participantDid,
|
|
855
1105
|
cohortId,
|
|
856
1106
|
beaconType: state.config.beaconType,
|
|
857
1107
|
signalBytesHex,
|
|
858
|
-
casAnnouncement:
|
|
859
|
-
smtProof:
|
|
1108
|
+
casAnnouncement: payload.casAnnouncement,
|
|
1109
|
+
smtProof: payload.smtProof
|
|
860
1110
|
}));
|
|
861
1111
|
}
|
|
862
1112
|
return messages;
|
|
@@ -918,6 +1168,14 @@ var AggregationService = class {
|
|
|
918
1168
|
});
|
|
919
1169
|
state.signingSession = session;
|
|
920
1170
|
state.phase = "SigningStarted" /* SigningStarted */;
|
|
1171
|
+
const prevOutScript = txData.prevOutScripts[0];
|
|
1172
|
+
if (!prevOutScript) {
|
|
1173
|
+
throw new AggregationServiceError(
|
|
1174
|
+
`Cannot start signing for cohort ${cohortId}: txData.prevOutScripts[0] is missing.`,
|
|
1175
|
+
"MISSING_PREV_OUT_SCRIPT",
|
|
1176
|
+
{ cohortId }
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
921
1179
|
const messages = [];
|
|
922
1180
|
for (const participantDid of state.cohort.participants) {
|
|
923
1181
|
messages.push(createAuthorizationRequestMessage({
|
|
@@ -925,7 +1183,8 @@ var AggregationService = class {
|
|
|
925
1183
|
to: participantDid,
|
|
926
1184
|
cohortId,
|
|
927
1185
|
sessionId: session.id,
|
|
928
|
-
pendingTx: txData.tx.
|
|
1186
|
+
pendingTx: txData.tx.hex,
|
|
1187
|
+
prevOutScriptHex: (0, import_utils2.bytesToHex)(prevOutScript),
|
|
929
1188
|
prevOutValue: txData.prevOutValues[0]?.toString() ?? "0"
|
|
930
1189
|
}));
|
|
931
1190
|
}
|
|
@@ -989,7 +1248,7 @@ var AggregationService = class {
|
|
|
989
1248
|
state.signingSession.addPartialSignature(message.from, partialSignature);
|
|
990
1249
|
if (state.signingSession.partialSignatures.size === state.cohort.participants.length) {
|
|
991
1250
|
const signature = state.signingSession.generateFinalSignature();
|
|
992
|
-
state.signingSession.pendingTx.
|
|
1251
|
+
state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
|
|
993
1252
|
state.result = {
|
|
994
1253
|
cohortId,
|
|
995
1254
|
signature,
|
|
@@ -1018,13 +1277,19 @@ var AggregationService = class {
|
|
|
1018
1277
|
get cohorts() {
|
|
1019
1278
|
return [...this.#cohortStates.values()].map((s) => s.cohort);
|
|
1020
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Remove a cohort from the state map. Used by runners to GC state on cohort
|
|
1282
|
+
* completion, failure, or expiry. No-op if the cohort doesn't exist.
|
|
1283
|
+
*/
|
|
1284
|
+
removeCohort(cohortId) {
|
|
1285
|
+
this.#cohortStates.delete(cohortId);
|
|
1286
|
+
}
|
|
1021
1287
|
};
|
|
1022
1288
|
|
|
1023
1289
|
// src/core/aggregation/participant.ts
|
|
1024
|
-
var
|
|
1025
|
-
var import_smt2 = require("@did-btcr2/smt");
|
|
1290
|
+
var import_common5 = require("@did-btcr2/common");
|
|
1026
1291
|
var import_utils3 = require("@noble/hashes/utils");
|
|
1027
|
-
var
|
|
1292
|
+
var import_btc_signer3 = require("@scure/btc-signer");
|
|
1028
1293
|
var AggregationParticipant = class {
|
|
1029
1294
|
did;
|
|
1030
1295
|
keys;
|
|
@@ -1039,6 +1304,9 @@ var AggregationParticipant = class {
|
|
|
1039
1304
|
* outgoing messages — those come exclusively from action methods.
|
|
1040
1305
|
*/
|
|
1041
1306
|
receive(message) {
|
|
1307
|
+
if (message.version === void 0 || message.version !== AGGREGATION_WIRE_VERSION) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1042
1310
|
const type = message.type;
|
|
1043
1311
|
switch (type) {
|
|
1044
1312
|
case COHORT_ADVERT:
|
|
@@ -1195,42 +1463,26 @@ var AggregationParticipant = class {
|
|
|
1195
1463
|
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
|
|
1196
1464
|
if (!state.submittedUpdate) return;
|
|
1197
1465
|
const beaconType = message.body?.beaconType;
|
|
1466
|
+
if (!beaconType) return;
|
|
1467
|
+
const strategy = getBeaconStrategy(beaconType);
|
|
1468
|
+
if (!strategy) return;
|
|
1198
1469
|
const signalBytesHex = message.body?.signalBytesHex ?? "";
|
|
1199
|
-
const expectedHash = (0,
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
}
|
|
1215
|
-
const smtProof = message.body?.smtProof;
|
|
1216
|
-
if (smtProof?.updateId && smtProof?.nonce) {
|
|
1217
|
-
const canonicalBytes = new TextEncoder().encode((0, import_common3.canonicalize)(state.submittedUpdate));
|
|
1218
|
-
const expectedUpdateId = (0, import_smt2.hashToHex)((0, import_smt2.blockHash)(canonicalBytes));
|
|
1219
|
-
if (smtProof.updateId === expectedUpdateId) {
|
|
1220
|
-
const index = (0, import_smt2.didToIndex)(this.did);
|
|
1221
|
-
const candidateHash = (0, import_smt2.blockHash)((0, import_smt2.blockHash)((0, import_smt2.hexToHash)(smtProof.nonce)), (0, import_smt2.hexToHash)(smtProof.updateId));
|
|
1222
|
-
matches = (0, import_smt2.verifySerializedProof)(smtProof, index, candidateHash);
|
|
1223
|
-
}
|
|
1224
|
-
state.validation = {
|
|
1225
|
-
cohortId,
|
|
1226
|
-
beaconType,
|
|
1227
|
-
signalBytesHex,
|
|
1228
|
-
smtProof,
|
|
1229
|
-
expectedHash,
|
|
1230
|
-
matches
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1470
|
+
const expectedHash = (0, import_common5.canonicalHash)(state.submittedUpdate);
|
|
1471
|
+
const result = strategy.validateParticipantView({
|
|
1472
|
+
participantDid: this.did,
|
|
1473
|
+
submittedUpdate: state.submittedUpdate,
|
|
1474
|
+
expectedHash,
|
|
1475
|
+
body: message.body
|
|
1476
|
+
});
|
|
1477
|
+
state.validation = {
|
|
1478
|
+
cohortId,
|
|
1479
|
+
beaconType,
|
|
1480
|
+
signalBytesHex,
|
|
1481
|
+
expectedHash,
|
|
1482
|
+
matches: result.matches,
|
|
1483
|
+
casAnnouncement: result.casAnnouncement,
|
|
1484
|
+
smtProof: result.smtProof
|
|
1485
|
+
};
|
|
1234
1486
|
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
1235
1487
|
}
|
|
1236
1488
|
/**
|
|
@@ -1291,12 +1543,14 @@ var AggregationParticipant = class {
|
|
|
1291
1543
|
if (state.phase !== "ValidationSent" /* ValidationSent */) return;
|
|
1292
1544
|
const sessionId = message.body?.sessionId;
|
|
1293
1545
|
const pendingTxHex = message.body?.pendingTx;
|
|
1546
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
1294
1547
|
const prevOutValue = message.body?.prevOutValue;
|
|
1295
|
-
if (!sessionId || !pendingTxHex || !prevOutValue) return;
|
|
1548
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
|
|
1296
1549
|
state.signingRequest = {
|
|
1297
1550
|
cohortId,
|
|
1298
1551
|
sessionId,
|
|
1299
1552
|
pendingTxHex,
|
|
1553
|
+
prevOutScriptHex,
|
|
1300
1554
|
prevOutValue
|
|
1301
1555
|
};
|
|
1302
1556
|
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
@@ -1320,8 +1574,8 @@ var AggregationParticipant = class {
|
|
|
1320
1574
|
{ cohortId }
|
|
1321
1575
|
);
|
|
1322
1576
|
}
|
|
1323
|
-
const tx =
|
|
1324
|
-
const prevOutScripts =
|
|
1577
|
+
const tx = import_btc_signer3.Transaction.fromRaw((0, import_utils3.hexToBytes)(state.signingRequest.pendingTxHex));
|
|
1578
|
+
const prevOutScripts = [(0, import_utils3.hexToBytes)(state.signingRequest.prevOutScriptHex)];
|
|
1325
1579
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
1326
1580
|
const session = new BeaconSigningSession({
|
|
1327
1581
|
id: state.signingRequest.sessionId,
|
|
@@ -1391,6 +1645,67 @@ var AggregationParticipant = class {
|
|
|
1391
1645
|
}
|
|
1392
1646
|
};
|
|
1393
1647
|
|
|
1648
|
+
// src/core/aggregation/logger.ts
|
|
1649
|
+
var CONSOLE_LOGGER = {
|
|
1650
|
+
debug: (msg, ...args) => console.debug(msg, ...args),
|
|
1651
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
1652
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
1653
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
1654
|
+
};
|
|
1655
|
+
var SILENT_LOGGER = {
|
|
1656
|
+
debug: () => {
|
|
1657
|
+
},
|
|
1658
|
+
info: () => {
|
|
1659
|
+
},
|
|
1660
|
+
warn: () => {
|
|
1661
|
+
},
|
|
1662
|
+
error: () => {
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
// src/core/aggregation/messages/bodies.ts
|
|
1667
|
+
var hasStr = (b, k) => !!b && typeof b[k] === "string";
|
|
1668
|
+
var hasNum = (b, k) => !!b && typeof b[k] === "number";
|
|
1669
|
+
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
1670
|
+
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
1671
|
+
var hasBytesArray = (b, k) => {
|
|
1672
|
+
const v = b ? b[k] : void 0;
|
|
1673
|
+
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
1674
|
+
};
|
|
1675
|
+
function isCohortAdvertMessage(m) {
|
|
1676
|
+
return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasNum(m.body, "cohortSize") && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasBytes(m.body, "communicationPk");
|
|
1677
|
+
}
|
|
1678
|
+
function isCohortOptInMessage(m) {
|
|
1679
|
+
return m.type === COHORT_OPT_IN && hasStr(m.body, "cohortId") && hasBytes(m.body, "participantPk") && hasBytes(m.body, "communicationPk");
|
|
1680
|
+
}
|
|
1681
|
+
function isCohortOptInAcceptMessage(m) {
|
|
1682
|
+
return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, "cohortId");
|
|
1683
|
+
}
|
|
1684
|
+
function isCohortReadyMessage(m) {
|
|
1685
|
+
return m.type === COHORT_READY && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconAddress") && hasBytesArray(m.body, "cohortKeys");
|
|
1686
|
+
}
|
|
1687
|
+
function isSubmitUpdateMessage(m) {
|
|
1688
|
+
return m.type === SUBMIT_UPDATE && hasStr(m.body, "cohortId") && !!m.body && typeof m.body.signedUpdate === "object";
|
|
1689
|
+
}
|
|
1690
|
+
function isDistributeAggregatedDataMessage(m) {
|
|
1691
|
+
return m.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconType") && hasStr(m.body, "signalBytesHex");
|
|
1692
|
+
}
|
|
1693
|
+
function isValidationAckMessage(m) {
|
|
1694
|
+
return m.type === VALIDATION_ACK && hasStr(m.body, "cohortId") && hasBool(m.body, "approved");
|
|
1695
|
+
}
|
|
1696
|
+
function isAuthorizationRequestMessage(m) {
|
|
1697
|
+
return m.type === AUTHORIZATION_REQUEST && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasStr(m.body, "pendingTx") && hasStr(m.body, "prevOutScriptHex") && hasStr(m.body, "prevOutValue");
|
|
1698
|
+
}
|
|
1699
|
+
function isNonceContributionMessage(m) {
|
|
1700
|
+
return m.type === NONCE_CONTRIBUTION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "nonceContribution");
|
|
1701
|
+
}
|
|
1702
|
+
function isAggregatedNonceMessage(m) {
|
|
1703
|
+
return m.type === AGGREGATED_NONCE && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "aggregatedNonce");
|
|
1704
|
+
}
|
|
1705
|
+
function isSignatureAuthorizationMessage(m) {
|
|
1706
|
+
return m.type === SIGNATURE_AUTHORIZATION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "partialSignature");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1394
1709
|
// src/core/aggregation/messages/guards.ts
|
|
1395
1710
|
var KEYGEN_VALUES = /* @__PURE__ */ new Set([
|
|
1396
1711
|
COHORT_ADVERT,
|
|
@@ -1423,20 +1738,20 @@ function isSignMessageType(type) {
|
|
|
1423
1738
|
}
|
|
1424
1739
|
|
|
1425
1740
|
// src/core/aggregation/transport/error.ts
|
|
1426
|
-
var
|
|
1427
|
-
var TransportError = class extends
|
|
1741
|
+
var import_common6 = require("@did-btcr2/common");
|
|
1742
|
+
var TransportError = class extends import_common6.MethodError {
|
|
1428
1743
|
constructor(message, type = "TransportError", data) {
|
|
1429
1744
|
super(message, type, data);
|
|
1430
1745
|
}
|
|
1431
1746
|
};
|
|
1432
|
-
var TransportAdapterError = class extends
|
|
1747
|
+
var TransportAdapterError = class extends import_common6.MethodError {
|
|
1433
1748
|
constructor(message, type = "TransportAdapterError", data) {
|
|
1434
1749
|
super(message, type, data);
|
|
1435
1750
|
}
|
|
1436
1751
|
};
|
|
1437
1752
|
|
|
1438
1753
|
// src/core/aggregation/transport/factory.ts
|
|
1439
|
-
var
|
|
1754
|
+
var import_common7 = require("@did-btcr2/common");
|
|
1440
1755
|
|
|
1441
1756
|
// src/core/aggregation/transport/nostr.ts
|
|
1442
1757
|
var import_keypair = require("@did-btcr2/keypair");
|
|
@@ -1449,6 +1764,7 @@ var DEFAULT_NOSTR_RELAYS = [
|
|
|
1449
1764
|
"wss://relay.snort.social",
|
|
1450
1765
|
"wss://nostr-pub.wellorder.net"
|
|
1451
1766
|
];
|
|
1767
|
+
var DEFAULT_BROADCAST_LOOKBACK_MS = 5 * 60 * 1e3;
|
|
1452
1768
|
var NostrTransport = class _NostrTransport {
|
|
1453
1769
|
name = "nostr";
|
|
1454
1770
|
pool;
|
|
@@ -1456,8 +1772,12 @@ var NostrTransport = class _NostrTransport {
|
|
|
1456
1772
|
#actors = /* @__PURE__ */ new Map();
|
|
1457
1773
|
#peerRegistry = /* @__PURE__ */ new Map();
|
|
1458
1774
|
#started = false;
|
|
1775
|
+
#logger;
|
|
1776
|
+
#broadcastLookbackMs;
|
|
1459
1777
|
constructor(config) {
|
|
1460
1778
|
this.#relays = config?.relays ?? DEFAULT_NOSTR_RELAYS;
|
|
1779
|
+
this.#logger = config?.logger ?? CONSOLE_LOGGER;
|
|
1780
|
+
this.#broadcastLookbackMs = config?.broadcastLookbackMs ?? DEFAULT_BROADCAST_LOOKBACK_MS;
|
|
1461
1781
|
}
|
|
1462
1782
|
/**
|
|
1463
1783
|
* Registers an actor (DID + keys) to send/receive messages with.
|
|
@@ -1468,19 +1788,68 @@ var NostrTransport = class _NostrTransport {
|
|
|
1468
1788
|
* @example
|
|
1469
1789
|
* const transport = new NostrTransport();
|
|
1470
1790
|
* const keys = SchnorrKeyPair.generate();
|
|
1471
|
-
*
|
|
1791
|
+
* const did = DidBtcr2.create(keys.publicKey.compressed, { idType: 'KEY', network: 'mutinynet' });
|
|
1792
|
+
* transport.registerActor(did, keys);
|
|
1472
1793
|
* transport.start();
|
|
1473
1794
|
*/
|
|
1474
1795
|
registerActor(did, keys) {
|
|
1475
|
-
const entry = { keys, handlers: /* @__PURE__ */ new Map() };
|
|
1796
|
+
const entry = { keys, handlers: /* @__PURE__ */ new Map(), subscriptions: [] };
|
|
1476
1797
|
this.#actors.set(did, entry);
|
|
1477
1798
|
if (this.#started && this.pool) {
|
|
1478
1799
|
this.#subscribeDirected(did, entry);
|
|
1479
1800
|
}
|
|
1480
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Detach an actor: drop its handlers, close its relay subscriptions, and remove its keys + peer
|
|
1804
|
+
* mapping. Idempotent.
|
|
1805
|
+
* @param {string} did - The DID of the actor to unregister.
|
|
1806
|
+
* @returns {void}
|
|
1807
|
+
* @throws {TransportAdapterError} If the transport is not started or if the pool is unavailable.
|
|
1808
|
+
* @example
|
|
1809
|
+
* transport.unregisterActor(did);
|
|
1810
|
+
*/
|
|
1811
|
+
unregisterActor(did) {
|
|
1812
|
+
const entry = this.#actors.get(did);
|
|
1813
|
+
if (!entry) return;
|
|
1814
|
+
for (const sub of entry.subscriptions) {
|
|
1815
|
+
try {
|
|
1816
|
+
sub.close();
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
this.#logger.debug(`Error closing subscription for ${did}:`, err);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
entry.subscriptions.length = 0;
|
|
1822
|
+
entry.handlers.clear();
|
|
1823
|
+
this.#actors.delete(did);
|
|
1824
|
+
this.#peerRegistry.delete(did);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Remove a single (actor, messageType) handler. Idempotent.
|
|
1828
|
+
* @param {string} actorDid - The DID of the actor.
|
|
1829
|
+
* @param {string} messageType - The type of message to unregister the handler for.
|
|
1830
|
+
* @returns {void}
|
|
1831
|
+
* @example
|
|
1832
|
+
* transport.unregisterMessageHandler(actorDid, messageType);
|
|
1833
|
+
*/
|
|
1834
|
+
unregisterMessageHandler(actorDid, messageType) {
|
|
1835
|
+
const actor = this.#actors.get(actorDid);
|
|
1836
|
+
if (!actor) return;
|
|
1837
|
+
actor.handlers.delete(messageType);
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Gets the public key for a registered actor by their DID.
|
|
1841
|
+
* @param {string} did - The DID of the registered actor to get the public key for.
|
|
1842
|
+
* @returns {Uint8Array | undefined} The compressed public key bytes for the actor's DID, or
|
|
1843
|
+
* undefined if the DID is not registered.
|
|
1844
|
+
*/
|
|
1481
1845
|
getActorPk(did) {
|
|
1482
1846
|
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
1483
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Registers a peer's communication public key for encrypted messages.
|
|
1850
|
+
* @param {string} did - The DID of the peer to register.
|
|
1851
|
+
* @param {Uint8Array} communicationPk - The compressed secp256k1 public key bytes for the peer.
|
|
1852
|
+
*/
|
|
1484
1853
|
registerPeer(did, communicationPk) {
|
|
1485
1854
|
try {
|
|
1486
1855
|
new import_keypair.CompressedSecp256k1PublicKey(communicationPk);
|
|
@@ -1493,9 +1862,25 @@ var NostrTransport = class _NostrTransport {
|
|
|
1493
1862
|
}
|
|
1494
1863
|
this.#peerRegistry.set(did, communicationPk);
|
|
1495
1864
|
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Gets the registered communication public key for a peer by their DID.
|
|
1867
|
+
* @param {string} did - The DID of the peer to get the communication public key for.
|
|
1868
|
+
* @returns {Uint8Array | undefined} The compressed secp256k1 public key bytes for the peer, or
|
|
1869
|
+
* undefined if the peer is not registered.
|
|
1870
|
+
*/
|
|
1496
1871
|
getPeerPk(did) {
|
|
1497
1872
|
return this.#peerRegistry.get(did);
|
|
1498
1873
|
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Registers a message handler function for a specific actor and message type. The handler will be called
|
|
1876
|
+
* when a message of the specified type is received for the actor's DID. The transport must have been
|
|
1877
|
+
* started for handlers to be invoked. If the transport is already started, the handler will be registered
|
|
1878
|
+
* immediately; otherwise, it will be registered when the transport starts and the actor's subscription is created.
|
|
1879
|
+
* @param {string} actorDid - The DID of the actor to register the message handler for.
|
|
1880
|
+
* @param {string} messageType - The type of message to handle.
|
|
1881
|
+
* @param {MessageHandler} handler - The function to handle incoming messages of the specified type.
|
|
1882
|
+
* @throws {TransportAdapterError} If the actor DID is not registered or if the handler is invalid.
|
|
1883
|
+
*/
|
|
1499
1884
|
registerMessageHandler(actorDid, messageType, handler) {
|
|
1500
1885
|
const actor = this.#actors.get(actorDid);
|
|
1501
1886
|
if (!actor) {
|
|
@@ -1507,21 +1892,48 @@ var NostrTransport = class _NostrTransport {
|
|
|
1507
1892
|
}
|
|
1508
1893
|
actor.handlers.set(messageType, handler);
|
|
1509
1894
|
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Starts the transport by connecting to the configured Nostr relays and setting up subscriptions
|
|
1897
|
+
* for all registered actors. This method must be called after registering actors via registerActor()
|
|
1898
|
+
* and before sending or receiving messages. The transport will subscribe to broadcast events (kind 1)
|
|
1899
|
+
* for cohort adverts and directed events (kinds 1 and 1059) for each registered actor based on their
|
|
1900
|
+
* public keys. Incoming events are processed and dispatched to the appropriate handlers based on
|
|
1901
|
+
* message type. If the transport is already started, this method has no effect.
|
|
1902
|
+
* @returns {NostrTransport}
|
|
1903
|
+
*/
|
|
1510
1904
|
start() {
|
|
1511
1905
|
if (this.#started) return this;
|
|
1512
1906
|
this.#started = true;
|
|
1513
1907
|
this.pool = new import_pool.SimplePool();
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1908
|
+
const broadcastFilter = {
|
|
1909
|
+
kinds: [1],
|
|
1910
|
+
"#t": [COHORT_ADVERT]
|
|
1911
|
+
};
|
|
1912
|
+
if (this.#broadcastLookbackMs > 0) {
|
|
1913
|
+
broadcastFilter.since = Math.floor((Date.now() - this.#broadcastLookbackMs) / 1e3);
|
|
1914
|
+
}
|
|
1915
|
+
this.pool.subscribeMany(this.#relays, broadcastFilter, {
|
|
1916
|
+
onclose: (reasons) => this.#logger.debug("Nostr broadcast subscription closed", reasons),
|
|
1517
1917
|
onevent: this.#handleBroadcastEvent.bind(this)
|
|
1518
1918
|
});
|
|
1519
1919
|
for (const [did, entry] of this.#actors) {
|
|
1520
1920
|
this.#subscribeDirected(did, entry);
|
|
1521
1921
|
}
|
|
1522
|
-
|
|
1922
|
+
this.#logger.info(`NostrTransport started, listening on ${this.#relays.length} relay(s)`);
|
|
1523
1923
|
return this;
|
|
1524
1924
|
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Sends a message by publishing a Nostr event to the configured relays. The message is serialized
|
|
1927
|
+
* as JSON and included in the event content.
|
|
1928
|
+
* @param {BaseMessage} message - The aggregation message to send. Must include a valid `type` property.
|
|
1929
|
+
* @param {Did} sender - The DID of the registered actor sending the message. Must have been
|
|
1930
|
+
* registered via registerActor().
|
|
1931
|
+
* @param {Did} [to] - Optional recipient DID for directed messages. Required for encrypted message
|
|
1932
|
+
* types. If provided, must have been registered via registerPeer().
|
|
1933
|
+
* @returns {Promise<void>} Resolves when the message has been published to the relays. Note that
|
|
1934
|
+
* publication is best-effort: the method will resolve as long as at least one relay accepts the
|
|
1935
|
+
* event, even if others reject it.
|
|
1936
|
+
*/
|
|
1525
1937
|
async sendMessage(message, sender, to) {
|
|
1526
1938
|
const type = message.type;
|
|
1527
1939
|
if (!type) {
|
|
@@ -1558,7 +1970,7 @@ var NostrTransport = class _NostrTransport {
|
|
|
1558
1970
|
tags,
|
|
1559
1971
|
content: JSON.stringify(message, _NostrTransport.#jsonReplacer)
|
|
1560
1972
|
}, senderKeys.secretKey.bytes);
|
|
1561
|
-
|
|
1973
|
+
this.#logger.debug(`Publishing kind 1 [${type}]`);
|
|
1562
1974
|
await this.#publishToRelays(event);
|
|
1563
1975
|
return;
|
|
1564
1976
|
}
|
|
@@ -1590,25 +2002,70 @@ var NostrTransport = class _NostrTransport {
|
|
|
1590
2002
|
tags,
|
|
1591
2003
|
content
|
|
1592
2004
|
}, senderKeys.secretKey.bytes);
|
|
1593
|
-
|
|
2005
|
+
this.#logger.debug(`Publishing kind 1059 [${type}]`);
|
|
1594
2006
|
await this.#publishToRelays(event);
|
|
1595
2007
|
return;
|
|
1596
2008
|
}
|
|
1597
|
-
|
|
2009
|
+
this.#logger.warn(`Unsupported message type: ${type}`);
|
|
1598
2010
|
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Publish the message once immediately and then re-publish on an interval
|
|
2013
|
+
* until the returned stop function is invoked.
|
|
2014
|
+
*
|
|
2015
|
+
* Useful for broadcast messages (COHORT_ADVERT) on relays that don't
|
|
2016
|
+
* backfill historical events to late subscribers: republishing gives late
|
|
2017
|
+
* joiners a window to discover the message without requiring protocol
|
|
2018
|
+
* changes. Relay rate-limit / publish failures inside the interval are
|
|
2019
|
+
* caught and logged rather than propagated — the caller should stop the
|
|
2020
|
+
* repeater once the protocol condition is satisfied.
|
|
2021
|
+
*/
|
|
2022
|
+
publishRepeating(message, sender, intervalMs, recipient) {
|
|
2023
|
+
let stopped = false;
|
|
2024
|
+
void this.sendMessage(message, sender, recipient).catch((err) => {
|
|
2025
|
+
this.#logger.debug("publishRepeating first send failed:", err);
|
|
2026
|
+
});
|
|
2027
|
+
const timer = setInterval(() => {
|
|
2028
|
+
if (stopped) return;
|
|
2029
|
+
void this.sendMessage(message, sender, recipient).catch((err) => {
|
|
2030
|
+
this.#logger.debug("publishRepeating retry failed:", err);
|
|
2031
|
+
});
|
|
2032
|
+
}, intervalMs);
|
|
2033
|
+
return () => {
|
|
2034
|
+
if (stopped) return;
|
|
2035
|
+
stopped = true;
|
|
2036
|
+
clearInterval(timer);
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Creates a directed subscription for the given actor, filtering for messages that match the
|
|
2041
|
+
* actor's public key. Messages received on this subscription are dispatched to the actor's
|
|
2042
|
+
* registered handlers based on message type.
|
|
2043
|
+
* @param {string} did - The DID of the actor to create the subscription for.
|
|
2044
|
+
* @param {ActorEntry} entry - The actor's registration entry containing keys and handlers.
|
|
2045
|
+
* @returns {void}
|
|
2046
|
+
* @throws {TransportAdapterError} If the transport is not started or if the pool is unavailable.
|
|
2047
|
+
*/
|
|
1599
2048
|
#subscribeDirected(did, entry) {
|
|
1600
2049
|
if (!this.pool) return;
|
|
1601
2050
|
const pkHex = (0, import_utils4.bytesToHex)(entry.keys.publicKey.x);
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
onclose: (reasons) => console.debug(`Nostr directed subscription closed for ${did}`, reasons),
|
|
2051
|
+
const sub = this.pool.subscribeMany(this.#relays, { kinds: [1, 1059], "#p": [pkHex] }, {
|
|
2052
|
+
onclose: (reasons) => this.#logger.debug(`Nostr directed subscription closed for ${did}`, reasons),
|
|
1605
2053
|
onevent: this.#makeActorEventHandler(did)
|
|
1606
2054
|
});
|
|
2055
|
+
entry.subscriptions.push(sub);
|
|
1607
2056
|
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Creates an event handler function for a specific actor that processes incoming events, decrypts
|
|
2059
|
+
* if necessary, and dispatches messages to the actor's registered handlers based on message type.
|
|
2060
|
+
* @param {string} actorDid - The DID of the actor to create the event handler for.
|
|
2061
|
+
* @returns {(event: Event) => Promise<void>} An asynchronous event handler function that
|
|
2062
|
+
* processes incoming events for the specified actor.
|
|
2063
|
+
*/
|
|
1608
2064
|
#makeActorEventHandler(actorDid) {
|
|
1609
2065
|
return async (event) => {
|
|
1610
2066
|
const actor = this.#actors.get(actorDid);
|
|
1611
2067
|
if (!actor) return;
|
|
2068
|
+
if (event.pubkey === (0, import_utils4.bytesToHex)(actor.keys.publicKey.x)) return;
|
|
1612
2069
|
let message;
|
|
1613
2070
|
try {
|
|
1614
2071
|
if (event.kind === 1) {
|
|
@@ -1624,19 +2081,29 @@ var NostrTransport = class _NostrTransport {
|
|
|
1624
2081
|
return;
|
|
1625
2082
|
}
|
|
1626
2083
|
} catch (err) {
|
|
1627
|
-
|
|
2084
|
+
this.#logger.debug(`Failed to parse event ${event.id} for ${actorDid}:`, err);
|
|
1628
2085
|
return;
|
|
1629
2086
|
}
|
|
1630
2087
|
this.#dispatchMessage(message, actor);
|
|
1631
2088
|
};
|
|
1632
2089
|
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Handles incoming broadcast events (kind 1) by parsing the event content, validating it as an
|
|
2092
|
+
* aggregation message, and dispatching it to all registered actors that have handlers for the
|
|
2093
|
+
* message type. This is used for COHORT_ADVERT messages that need to be received by all actors
|
|
2094
|
+
* regardless of DID.
|
|
2095
|
+
* @param {Event} event - The Nostr event to handle, expected to be a kind 1 broadcast containing
|
|
2096
|
+
* a COHORT_ADVERT message. The event content is parsed and dispatched to all registered actors
|
|
2097
|
+
* that have handlers for the
|
|
2098
|
+
* @returns
|
|
2099
|
+
*/
|
|
1633
2100
|
async #handleBroadcastEvent(event) {
|
|
1634
2101
|
if (event.kind !== 1) return;
|
|
1635
2102
|
let message;
|
|
1636
2103
|
try {
|
|
1637
2104
|
message = JSON.parse(event.content, _NostrTransport.#jsonReviver);
|
|
1638
2105
|
} catch (err) {
|
|
1639
|
-
|
|
2106
|
+
this.#logger.debug(`Failed to parse broadcast event ${event.id}:`, err);
|
|
1640
2107
|
return;
|
|
1641
2108
|
}
|
|
1642
2109
|
if (message.body && typeof message.body === "object") {
|
|
@@ -1649,6 +2116,18 @@ var NostrTransport = class _NostrTransport {
|
|
|
1649
2116
|
if (handler) await handler(message);
|
|
1650
2117
|
}
|
|
1651
2118
|
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Dispatches a parsed message to the appropriate handler of a registered actor based on message type.
|
|
2121
|
+
* The message is expected to have already been parsed from the Nostr event content and validated as
|
|
2122
|
+
* an aggregation message. If the message has a body, its properties are merged into the top-level
|
|
2123
|
+
* message object for easier handler access. The message is then dispatched to the handler registered
|
|
2124
|
+
* for its type, if one exists.
|
|
2125
|
+
* @param {Record<string, unknown>} message - The message object parsed from a Nostr event, expected to
|
|
2126
|
+
* @param {ActorEntry} actor - The registered actor entry containing keys and handlers to dispatch the message to.
|
|
2127
|
+
* @returns {void}
|
|
2128
|
+
* @throws {TransportAdapterError} If the message type is unsupported or if no handler is registered
|
|
2129
|
+
* for the message type.
|
|
2130
|
+
*/
|
|
1652
2131
|
#dispatchMessage(message, actor) {
|
|
1653
2132
|
if (message.body && typeof message.body === "object") {
|
|
1654
2133
|
message = { ...message, ...message.body };
|
|
@@ -1658,6 +2137,16 @@ var NostrTransport = class _NostrTransport {
|
|
|
1658
2137
|
const handler = actor.handlers.get(messageType);
|
|
1659
2138
|
if (handler) handler(message);
|
|
1660
2139
|
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Publishes a Nostr event to the configured relays and handles the results. The method waits for all
|
|
2142
|
+
* relay promises to settle and checks how many accepted or rejected the event. If all relays reject the event,
|
|
2143
|
+
* an error is thrown. Otherwise, the method completes successfully even if some relays rejected the event,
|
|
2144
|
+
* as long as at least one relay accepted it. Relay rejections are logged for debugging purposes.
|
|
2145
|
+
* @param {Event} event - The Nostr event to publish to the configured relays. The event should already
|
|
2146
|
+
* @returns {Promise<void>} A promise that resolves if the event was accepted by at least one relay, or rejects
|
|
2147
|
+
* with a TransportAdapterError if all relays rejected the event.
|
|
2148
|
+
* @throws {TransportAdapterError} If the pool is not initialized or if all relays reject the event.
|
|
2149
|
+
*/
|
|
1661
2150
|
async #publishToRelays(event) {
|
|
1662
2151
|
const relayPromises = this.pool?.publish(this.#relays, event);
|
|
1663
2152
|
if (!relayPromises?.length) return;
|
|
@@ -1665,7 +2154,7 @@ var NostrTransport = class _NostrTransport {
|
|
|
1665
2154
|
const accepted = results.filter((r) => r.status === "fulfilled").length;
|
|
1666
2155
|
const rejected = results.filter((r) => r.status === "rejected");
|
|
1667
2156
|
for (const r of rejected) {
|
|
1668
|
-
|
|
2157
|
+
this.#logger.debug(`Relay rejected event ${event.id}: ${r.reason}`);
|
|
1669
2158
|
}
|
|
1670
2159
|
if (accepted === 0) {
|
|
1671
2160
|
throw new TransportAdapterError(
|
|
@@ -1675,12 +2164,36 @@ var NostrTransport = class _NostrTransport {
|
|
|
1675
2164
|
);
|
|
1676
2165
|
}
|
|
1677
2166
|
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Custom JSON replacer to handle serialization of Uint8Array values as hex strings in message
|
|
2169
|
+
* content. This allows messages containing binary data (e.g. public keys, signatures) to be correctly
|
|
2170
|
+
* serialized to JSON for Nostr event content. The replacer checks if a value is a Uint8Array and, if so,
|
|
2171
|
+
* converts it to a hex string wrapped in an object with a __bytes property. The corresponding reviver
|
|
2172
|
+
* can then convert this back to a Uint8Array when parsing the message content from the event.
|
|
2173
|
+
* @param {string} _key - The key of the property being processed.
|
|
2174
|
+
* @param {unknown} value - The value to check if the message type is valid.
|
|
2175
|
+
* @returns {unknown} The transformed value for JSON serialization. If the value is a Uint8Array,
|
|
2176
|
+
* it returns an object with a __bytes property containing the hex string. Otherwise, it returns
|
|
2177
|
+
* the value unchanged.
|
|
2178
|
+
*/
|
|
1678
2179
|
static #jsonReplacer(_key, value) {
|
|
1679
2180
|
if (value instanceof Uint8Array) {
|
|
1680
2181
|
return { __bytes: (0, import_utils4.bytesToHex)(value) };
|
|
1681
2182
|
}
|
|
1682
2183
|
return value;
|
|
1683
2184
|
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Custom JSON reviver to handle deserialization of hex strings back into Uint8Array values in message
|
|
2187
|
+
* content. This complements the custom replacer used during serialization, allowing messages that contain
|
|
2188
|
+
* binary data (e.g. public keys, signatures) to be correctly reconstructed when parsing JSON from
|
|
2189
|
+
* Nostr event content. The reviver checks if a value is an object with a __bytes property and,
|
|
2190
|
+
* if so, converts the hex string back into a Uint8Array. Otherwise, it returns the value unchanged.
|
|
2191
|
+
* @param {string} _key - The key of the property being processed.
|
|
2192
|
+
* @param {unknown} value - The value to check if it is an object containing a __bytes property for
|
|
2193
|
+
* hex string conversion.
|
|
2194
|
+
* @returns {unknown} The transformed value for JSON deserialization. If the value is an object
|
|
2195
|
+
* with a __bytes property, it returns a Uint8Array. Otherwise, it returns the value unchanged.
|
|
2196
|
+
*/
|
|
1684
2197
|
static #jsonReviver(_key, value) {
|
|
1685
2198
|
if (value && typeof value === "object" && "__bytes" in value) {
|
|
1686
2199
|
return (0, import_utils4.hexToBytes)(value.__bytes);
|
|
@@ -1696,7 +2209,7 @@ var TransportFactory = class {
|
|
|
1696
2209
|
case "nostr":
|
|
1697
2210
|
return new NostrTransport({ relays: config.relays });
|
|
1698
2211
|
case "didcomm":
|
|
1699
|
-
throw new
|
|
2212
|
+
throw new import_common7.NotImplementedError("DIDComm transport not implemented yet.");
|
|
1700
2213
|
default:
|
|
1701
2214
|
throw new TransportError(
|
|
1702
2215
|
`Invalid transport type: ${config.type}`,
|
|
@@ -1708,29 +2221,38 @@ var TransportFactory = class {
|
|
|
1708
2221
|
};
|
|
1709
2222
|
|
|
1710
2223
|
// src/core/aggregation/transport/didcomm.ts
|
|
1711
|
-
var
|
|
2224
|
+
var import_common8 = require("@did-btcr2/common");
|
|
1712
2225
|
var DidCommTransport = class {
|
|
1713
2226
|
name = "didcomm";
|
|
1714
2227
|
start() {
|
|
1715
|
-
throw new
|
|
2228
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented. Use NostrTransport instead.");
|
|
1716
2229
|
}
|
|
1717
2230
|
registerActor(_did, _keys) {
|
|
1718
|
-
throw new
|
|
2231
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1719
2232
|
}
|
|
1720
2233
|
getActorPk(_did) {
|
|
1721
|
-
throw new
|
|
2234
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1722
2235
|
}
|
|
1723
2236
|
registerPeer(_did, _communicationPk) {
|
|
1724
|
-
throw new
|
|
2237
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1725
2238
|
}
|
|
1726
2239
|
getPeerPk(_did) {
|
|
1727
|
-
throw new
|
|
2240
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1728
2241
|
}
|
|
1729
2242
|
registerMessageHandler(_actorDid, _messageType, _handler) {
|
|
1730
|
-
throw new
|
|
2243
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
2244
|
+
}
|
|
2245
|
+
unregisterMessageHandler(_actorDid, _messageType) {
|
|
2246
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
2247
|
+
}
|
|
2248
|
+
unregisterActor(_did) {
|
|
2249
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1731
2250
|
}
|
|
1732
2251
|
async sendMessage(_message, _sender, _recipient) {
|
|
1733
|
-
throw new
|
|
2252
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
2253
|
+
}
|
|
2254
|
+
publishRepeating(_message, _sender, _intervalMs, _recipient) {
|
|
2255
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1734
2256
|
}
|
|
1735
2257
|
};
|
|
1736
2258
|
|
|
@@ -1789,7 +2311,8 @@ var TypedEventEmitter = class {
|
|
|
1789
2311
|
};
|
|
1790
2312
|
|
|
1791
2313
|
// src/core/aggregation/runner/service-runner.ts
|
|
1792
|
-
var
|
|
2314
|
+
var DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 6e4;
|
|
2315
|
+
var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEventEmitter {
|
|
1793
2316
|
/** Direct access to the underlying state machine for advanced use. */
|
|
1794
2317
|
session;
|
|
1795
2318
|
#transport;
|
|
@@ -1798,11 +2321,26 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1798
2321
|
#onOptInReceived;
|
|
1799
2322
|
#onReadyToFinalize;
|
|
1800
2323
|
#onProvideTxData;
|
|
2324
|
+
#cohortTtlMs;
|
|
2325
|
+
#phaseTimeoutMs;
|
|
2326
|
+
#advertRepeatIntervalMs;
|
|
1801
2327
|
#cohortId;
|
|
1802
2328
|
#handlersRegistered = false;
|
|
1803
2329
|
#stopped = false;
|
|
2330
|
+
/**
|
|
2331
|
+
* Guard against the async race where two concurrent #handleOptIn invocations
|
|
2332
|
+
* both pass the `participants.length >= minParticipants` check before either
|
|
2333
|
+
* mutates the cohort phase. Set synchronously before any `await` so subsequent
|
|
2334
|
+
* handlers observe it on their next resumption.
|
|
2335
|
+
*/
|
|
2336
|
+
#finalizing = false;
|
|
1804
2337
|
#resolveRun;
|
|
1805
2338
|
#rejectRun;
|
|
2339
|
+
#cohortTtlTimer;
|
|
2340
|
+
#phaseTimer;
|
|
2341
|
+
#lastObservedPhase;
|
|
2342
|
+
/** Stop handle for the repeating COHORT_ADVERT publish loop. */
|
|
2343
|
+
#stopAdvertRepeat;
|
|
1806
2344
|
constructor(options) {
|
|
1807
2345
|
super();
|
|
1808
2346
|
this.#transport = options.transport;
|
|
@@ -1813,7 +2351,25 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1813
2351
|
finalize: acceptedCount >= minRequired
|
|
1814
2352
|
}));
|
|
1815
2353
|
this.#onProvideTxData = options.onProvideTxData;
|
|
1816
|
-
this
|
|
2354
|
+
this.#cohortTtlMs = options.cohortTtlMs;
|
|
2355
|
+
this.#phaseTimeoutMs = options.phaseTimeoutMs;
|
|
2356
|
+
this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
2357
|
+
this.session = new AggregationService({
|
|
2358
|
+
did: options.did,
|
|
2359
|
+
keys: options.keys,
|
|
2360
|
+
maxUpdateSizeBytes: options.maxUpdateSizeBytes
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Drain any silent rejections the state machine recorded during the most
|
|
2365
|
+
* recent receive() and surface them as `message-rejected` events. Safe to
|
|
2366
|
+
* call even before a cohortId is assigned.
|
|
2367
|
+
*/
|
|
2368
|
+
#drainRejections() {
|
|
2369
|
+
if (!this.#cohortId) return;
|
|
2370
|
+
for (const r of this.session.drainRejections(this.#cohortId)) {
|
|
2371
|
+
this.emit("message-rejected", { cohortId: this.#cohortId, ...r });
|
|
2372
|
+
}
|
|
1817
2373
|
}
|
|
1818
2374
|
/**
|
|
1819
2375
|
* Run the protocol to completion. Resolves with the final aggregation result
|
|
@@ -1828,22 +2384,103 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1828
2384
|
try {
|
|
1829
2385
|
this.#registerHandlers();
|
|
1830
2386
|
this.#cohortId = this.session.createCohort(this.#config);
|
|
2387
|
+
this.#startTimers();
|
|
1831
2388
|
const advertMsgs = this.session.advertise(this.#cohortId);
|
|
2389
|
+
this.#onPhaseMaybeChanged();
|
|
1832
2390
|
this.emit("cohort-advertised", { cohortId: this.#cohortId });
|
|
1833
|
-
this.#
|
|
2391
|
+
if (this.#advertRepeatIntervalMs > 0) {
|
|
2392
|
+
this.#startAdvertRepeat(advertMsgs);
|
|
2393
|
+
} else {
|
|
2394
|
+
this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
|
|
2395
|
+
}
|
|
1834
2396
|
} catch (err) {
|
|
1835
2397
|
this.#fail(err);
|
|
1836
2398
|
}
|
|
1837
2399
|
});
|
|
1838
2400
|
}
|
|
1839
2401
|
/**
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
2402
|
+
* Begin publishing the cohort advert immediately and on a repeating interval
|
|
2403
|
+
* until {@link #stopAdvertRepeating} is called. Each advert is broadcast
|
|
2404
|
+
* (no recipient) via the transport's `publishRepeating` primitive.
|
|
2405
|
+
*/
|
|
2406
|
+
#startAdvertRepeat(advertMsgs) {
|
|
2407
|
+
const stops = [];
|
|
2408
|
+
for (const msg of advertMsgs) {
|
|
2409
|
+
stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
|
|
2410
|
+
}
|
|
2411
|
+
this.#stopAdvertRepeat = () => {
|
|
2412
|
+
for (const stop of stops) {
|
|
2413
|
+
try {
|
|
2414
|
+
stop();
|
|
2415
|
+
} catch {
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
/** Stop the advert republish loop. Idempotent. */
|
|
2421
|
+
#stopAdvertRepeating() {
|
|
2422
|
+
if (!this.#stopAdvertRepeat) return;
|
|
2423
|
+
const stop = this.#stopAdvertRepeat;
|
|
2424
|
+
this.#stopAdvertRepeat = void 0;
|
|
2425
|
+
stop();
|
|
2426
|
+
}
|
|
2427
|
+
/** Schedule cohort TTL + phase timeout at the start of a run. */
|
|
2428
|
+
#startTimers() {
|
|
2429
|
+
if (this.#cohortTtlMs !== void 0) {
|
|
2430
|
+
this.#cohortTtlTimer = setTimeout(() => {
|
|
2431
|
+
const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
|
|
2432
|
+
this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
|
|
2433
|
+
this.#fail(new Error(reason));
|
|
2434
|
+
}, this.#cohortTtlMs);
|
|
2435
|
+
}
|
|
2436
|
+
this.#resetPhaseTimer();
|
|
2437
|
+
}
|
|
2438
|
+
/** Reset the per-phase stall timer. Called when a phase transition is observed. */
|
|
2439
|
+
#resetPhaseTimer() {
|
|
2440
|
+
if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
|
|
2441
|
+
this.#phaseTimer = void 0;
|
|
2442
|
+
if (this.#phaseTimeoutMs === void 0) return;
|
|
2443
|
+
this.#phaseTimer = setTimeout(() => {
|
|
2444
|
+
const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
2445
|
+
this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
|
|
2446
|
+
this.#fail(new Error(reason));
|
|
2447
|
+
}, this.#phaseTimeoutMs);
|
|
2448
|
+
}
|
|
2449
|
+
/** Detect a phase change since the last observation and reset the phase timer. */
|
|
2450
|
+
#onPhaseMaybeChanged() {
|
|
2451
|
+
if (!this.#cohortId) return;
|
|
2452
|
+
const phase = this.session.getCohortPhase(this.#cohortId);
|
|
2453
|
+
if (phase !== this.#lastObservedPhase) {
|
|
2454
|
+
this.#lastObservedPhase = phase;
|
|
2455
|
+
this.#resetPhaseTimer();
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
/** Clear both timers. Called on successful completion, stop(), and #fail. */
|
|
2459
|
+
#clearTimers() {
|
|
2460
|
+
if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
|
|
2461
|
+
if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
|
|
2462
|
+
this.#cohortTtlTimer = void 0;
|
|
2463
|
+
this.#phaseTimer = void 0;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Stop the runner early. Marks the runner stopped and detaches transport
|
|
2467
|
+
* handlers so a restart or a new runner doesn't inherit stale dispatch.
|
|
1843
2468
|
*/
|
|
1844
2469
|
stop() {
|
|
1845
2470
|
this.#stopped = true;
|
|
1846
|
-
|
|
2471
|
+
this.#stopAdvertRepeating();
|
|
2472
|
+
this.#clearTimers();
|
|
2473
|
+
this.#unregisterHandlers();
|
|
2474
|
+
if (this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
2475
|
+
}
|
|
2476
|
+
/** Message types this runner listens for on the transport. */
|
|
2477
|
+
static #HANDLED_MESSAGE_TYPES = [
|
|
2478
|
+
COHORT_OPT_IN,
|
|
2479
|
+
SUBMIT_UPDATE,
|
|
2480
|
+
VALIDATION_ACK,
|
|
2481
|
+
NONCE_CONTRIBUTION,
|
|
2482
|
+
SIGNATURE_AUTHORIZATION
|
|
2483
|
+
];
|
|
1847
2484
|
/**
|
|
1848
2485
|
* Internal: handler registration with the transport. Idempotent.
|
|
1849
2486
|
*/
|
|
@@ -1856,6 +2493,14 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1856
2493
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
1857
2494
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
1858
2495
|
}
|
|
2496
|
+
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
2497
|
+
#unregisterHandlers() {
|
|
2498
|
+
if (!this.#handlersRegistered) return;
|
|
2499
|
+
this.#handlersRegistered = false;
|
|
2500
|
+
for (const type of _AggregationServiceRunner.#HANDLED_MESSAGE_TYPES) {
|
|
2501
|
+
this.#transport.unregisterMessageHandler(this.#did, type);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
1859
2504
|
/**
|
|
1860
2505
|
* Internal: message handlers for each protocol step. Each handler:
|
|
1861
2506
|
* 1) feeds the message into the state machine via session.receive()
|
|
@@ -1872,6 +2517,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1872
2517
|
if (this.#stopped) return;
|
|
1873
2518
|
try {
|
|
1874
2519
|
this.session.receive(msg);
|
|
2520
|
+
this.#drainRejections();
|
|
2521
|
+
this.#onPhaseMaybeChanged();
|
|
1875
2522
|
const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
|
|
1876
2523
|
if (!optIn) return;
|
|
1877
2524
|
this.emit("opt-in-received", optIn);
|
|
@@ -1883,19 +2530,23 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1883
2530
|
await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
|
|
1884
2531
|
this.emit("participant-accepted", { participantDid: msg.from });
|
|
1885
2532
|
const cohort = this.session.getCohort(this.#cohortId);
|
|
1886
|
-
if (cohort.participants.length >= this.#config.minParticipants) {
|
|
2533
|
+
if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
|
|
2534
|
+
this.#finalizing = true;
|
|
1887
2535
|
const finalizeDecision = await this.#onReadyToFinalize({
|
|
1888
2536
|
acceptedCount: cohort.participants.length,
|
|
1889
2537
|
minRequired: this.#config.minParticipants
|
|
1890
2538
|
});
|
|
1891
|
-
if (finalizeDecision.finalize) {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
cohortId: this.#cohortId,
|
|
1895
|
-
beaconAddress: cohort.beaconAddress
|
|
1896
|
-
});
|
|
1897
|
-
await this.#sendAll(readyMsgs);
|
|
2539
|
+
if (!finalizeDecision.finalize) {
|
|
2540
|
+
this.#finalizing = false;
|
|
2541
|
+
return;
|
|
1898
2542
|
}
|
|
2543
|
+
const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
|
|
2544
|
+
this.#stopAdvertRepeating();
|
|
2545
|
+
this.emit("keygen-complete", {
|
|
2546
|
+
cohortId: this.#cohortId,
|
|
2547
|
+
beaconAddress: cohort.beaconAddress
|
|
2548
|
+
});
|
|
2549
|
+
await this.#sendAll(readyMsgs);
|
|
1899
2550
|
}
|
|
1900
2551
|
} catch (err) {
|
|
1901
2552
|
this.#fail(err);
|
|
@@ -1913,6 +2564,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1913
2564
|
if (this.#stopped) return;
|
|
1914
2565
|
try {
|
|
1915
2566
|
this.session.receive(msg);
|
|
2567
|
+
this.#drainRejections();
|
|
2568
|
+
this.#onPhaseMaybeChanged();
|
|
1916
2569
|
this.emit("update-received", { participantDid: msg.from });
|
|
1917
2570
|
if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
|
|
1918
2571
|
const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
|
|
@@ -1935,9 +2588,18 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1935
2588
|
if (this.#stopped) return;
|
|
1936
2589
|
try {
|
|
1937
2590
|
this.session.receive(msg);
|
|
2591
|
+
this.#drainRejections();
|
|
2592
|
+
this.#onPhaseMaybeChanged();
|
|
1938
2593
|
const approved = !!msg.body?.approved;
|
|
1939
2594
|
this.emit("validation-received", { participantDid: msg.from, approved });
|
|
1940
|
-
|
|
2595
|
+
const phase = this.session.getCohortPhase(this.#cohortId);
|
|
2596
|
+
if (phase === "Failed" /* Failed */) {
|
|
2597
|
+
const reason = `Validation rejected by participant ${msg.from}`;
|
|
2598
|
+
this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
|
|
2599
|
+
this.#fail(new Error(reason));
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (phase === "Validated" /* Validated */) {
|
|
1941
2603
|
const cohort = this.session.getCohort(this.#cohortId);
|
|
1942
2604
|
const txData = await this.#onProvideTxData({
|
|
1943
2605
|
cohortId: this.#cohortId,
|
|
@@ -1965,6 +2627,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1965
2627
|
if (this.#stopped) return;
|
|
1966
2628
|
try {
|
|
1967
2629
|
this.session.receive(msg);
|
|
2630
|
+
this.#drainRejections();
|
|
2631
|
+
this.#onPhaseMaybeChanged();
|
|
1968
2632
|
this.emit("nonce-received", { participantDid: msg.from });
|
|
1969
2633
|
if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
|
|
1970
2634
|
await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
|
|
@@ -1985,8 +2649,12 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1985
2649
|
if (this.#stopped) return;
|
|
1986
2650
|
try {
|
|
1987
2651
|
this.session.receive(msg);
|
|
2652
|
+
this.#drainRejections();
|
|
2653
|
+
this.#onPhaseMaybeChanged();
|
|
1988
2654
|
const result = this.session.getResult(this.#cohortId);
|
|
1989
2655
|
if (result) {
|
|
2656
|
+
this.#clearTimers();
|
|
2657
|
+
this.#unregisterHandlers();
|
|
1990
2658
|
this.emit("signing-complete", result);
|
|
1991
2659
|
this.#resolveRun?.(result);
|
|
1992
2660
|
}
|
|
@@ -2011,6 +2679,10 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
2011
2679
|
* @param {Error} err - The error to handle.
|
|
2012
2680
|
*/
|
|
2013
2681
|
#fail(err) {
|
|
2682
|
+
this.#stopAdvertRepeating();
|
|
2683
|
+
this.#clearTimers();
|
|
2684
|
+
this.#unregisterHandlers();
|
|
2685
|
+
if (this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
2014
2686
|
this.emit("error", err);
|
|
2015
2687
|
this.#rejectRun?.(err);
|
|
2016
2688
|
}
|
|
@@ -2045,9 +2717,27 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2045
2717
|
async start() {
|
|
2046
2718
|
this.#registerHandlers();
|
|
2047
2719
|
}
|
|
2048
|
-
/** Stop the runner
|
|
2720
|
+
/** Stop the runner and detach transport handlers. Safe to call repeatedly. */
|
|
2049
2721
|
stop() {
|
|
2050
2722
|
this.#stopped = true;
|
|
2723
|
+
this.#unregisterHandlers();
|
|
2724
|
+
}
|
|
2725
|
+
/** Message types this runner listens for on the transport. */
|
|
2726
|
+
static #HANDLED_MESSAGE_TYPES = [
|
|
2727
|
+
COHORT_ADVERT,
|
|
2728
|
+
COHORT_OPT_IN_ACCEPT,
|
|
2729
|
+
COHORT_READY,
|
|
2730
|
+
DISTRIBUTE_AGGREGATED_DATA,
|
|
2731
|
+
AUTHORIZATION_REQUEST,
|
|
2732
|
+
AGGREGATED_NONCE
|
|
2733
|
+
];
|
|
2734
|
+
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
2735
|
+
#unregisterHandlers() {
|
|
2736
|
+
if (!this.#handlersRegistered) return;
|
|
2737
|
+
this.#handlersRegistered = false;
|
|
2738
|
+
for (const type of _AggregationParticipantRunner.#HANDLED_MESSAGE_TYPES) {
|
|
2739
|
+
this.#transport.unregisterMessageHandler(this.#did, type);
|
|
2740
|
+
}
|
|
2051
2741
|
}
|
|
2052
2742
|
/**
|
|
2053
2743
|
* Single-shot helper: start, join the first cohort that passes `shouldJoin`,
|
|
@@ -2212,7 +2902,14 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2212
2902
|
if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
|
|
2213
2903
|
const info = this.session.joinedCohorts.get(cohortId);
|
|
2214
2904
|
if (info) {
|
|
2215
|
-
this.
|
|
2905
|
+
const validation = this.session.pendingValidations.get(cohortId);
|
|
2906
|
+
this.emit("cohort-complete", {
|
|
2907
|
+
cohortId,
|
|
2908
|
+
beaconAddress: info.beaconAddress,
|
|
2909
|
+
beaconType: validation?.beaconType ?? "",
|
|
2910
|
+
casAnnouncement: validation?.casAnnouncement,
|
|
2911
|
+
smtProof: validation?.smtProof
|
|
2912
|
+
});
|
|
2216
2913
|
}
|
|
2217
2914
|
}
|
|
2218
2915
|
} catch (err) {
|
|
@@ -2234,33 +2931,33 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2234
2931
|
};
|
|
2235
2932
|
|
|
2236
2933
|
// src/core/beacon/beacon.ts
|
|
2237
|
-
var
|
|
2934
|
+
var import_secp256k12 = require("@noble/secp256k1");
|
|
2238
2935
|
var import_utils5 = require("@noble/hashes/utils");
|
|
2239
|
-
var
|
|
2936
|
+
var import_btc_signer4 = require("@scure/btc-signer");
|
|
2240
2937
|
|
|
2241
2938
|
// src/core/beacon/error.ts
|
|
2242
|
-
var
|
|
2243
|
-
var BeaconError = class extends
|
|
2939
|
+
var import_common9 = require("@did-btcr2/common");
|
|
2940
|
+
var BeaconError = class extends import_common9.MethodError {
|
|
2244
2941
|
constructor(message, type = "BeaconError", data) {
|
|
2245
2942
|
super(message, type, data);
|
|
2246
2943
|
}
|
|
2247
2944
|
};
|
|
2248
|
-
var SingletonBeaconError = class extends
|
|
2945
|
+
var SingletonBeaconError = class extends import_common9.MethodError {
|
|
2249
2946
|
constructor(message, type = "SingletonBeaconError", data) {
|
|
2250
2947
|
super(message, type, data);
|
|
2251
2948
|
}
|
|
2252
2949
|
};
|
|
2253
|
-
var AggregateBeaconError = class extends
|
|
2950
|
+
var AggregateBeaconError = class extends import_common9.MethodError {
|
|
2254
2951
|
constructor(message, type = "AggregateBeaconError", data) {
|
|
2255
2952
|
super(message, type, data);
|
|
2256
2953
|
}
|
|
2257
2954
|
};
|
|
2258
|
-
var CASBeaconError = class extends
|
|
2955
|
+
var CASBeaconError = class extends import_common9.MethodError {
|
|
2259
2956
|
constructor(message, type = "CASBeaconError", data) {
|
|
2260
2957
|
super(message, type, data);
|
|
2261
2958
|
}
|
|
2262
2959
|
};
|
|
2263
|
-
var SMTBeaconError = class extends
|
|
2960
|
+
var SMTBeaconError = class extends import_common9.MethodError {
|
|
2264
2961
|
constructor(message, type = "SMTBeaconError", data) {
|
|
2265
2962
|
super(message, type, data);
|
|
2266
2963
|
}
|
|
@@ -2288,6 +2985,62 @@ var StaticFeeEstimator = class {
|
|
|
2288
2985
|
|
|
2289
2986
|
// src/core/beacon/beacon.ts
|
|
2290
2987
|
var DEFAULT_FEE_ESTIMATOR = new StaticFeeEstimator(5);
|
|
2988
|
+
var P2TR_BEACON_TX_VSIZE = 140;
|
|
2989
|
+
function opReturnScript(signalBytes) {
|
|
2990
|
+
return import_btc_signer4.Script.encode([import_btc_signer4.OP.RETURN, signalBytes]);
|
|
2991
|
+
}
|
|
2992
|
+
async function fetchSpendableUtxo(bitcoinAddress, bitcoin) {
|
|
2993
|
+
const utxos = await bitcoin.rest.address.getUtxos(bitcoinAddress);
|
|
2994
|
+
if (!utxos.length) {
|
|
2995
|
+
throw new BeaconError(
|
|
2996
|
+
"No UTXOs found, please fund address!",
|
|
2997
|
+
"UNFUNDED_BEACON_ADDRESS",
|
|
2998
|
+
{ bitcoinAddress }
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
const utxo = utxos.sort((a, b) => b.status.block_height - a.status.block_height).shift();
|
|
3002
|
+
if (!utxo) {
|
|
3003
|
+
throw new BeaconError(
|
|
3004
|
+
"Beacon bitcoin address unfunded or utxos unconfirmed.",
|
|
3005
|
+
"UNFUNDED_BEACON_ADDRESS",
|
|
3006
|
+
{ bitcoinAddress }
|
|
3007
|
+
);
|
|
3008
|
+
}
|
|
3009
|
+
const prevTxHex = await bitcoin.rest.transaction.getHex(utxo.txid);
|
|
3010
|
+
return { utxo, prevTxBytes: (0, import_utils5.hexToBytes)(prevTxHex) };
|
|
3011
|
+
}
|
|
3012
|
+
async function buildAggregationBeaconTx(opts) {
|
|
3013
|
+
const feeEstimator = opts.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
3014
|
+
const { utxo, prevTxBytes } = await fetchSpendableUtxo(opts.beaconAddress, opts.bitcoin);
|
|
3015
|
+
const tapOut = (0, import_btc_signer4.p2tr)(opts.internalPubkey, void 0, opts.network);
|
|
3016
|
+
const witnessScript = tapOut.script;
|
|
3017
|
+
const feeSats = await feeEstimator.estimateFee(P2TR_BEACON_TX_VSIZE);
|
|
3018
|
+
if (BigInt(utxo.value) <= feeSats) {
|
|
3019
|
+
throw new BeaconError(
|
|
3020
|
+
`UTXO value (${utxo.value}) insufficient to cover fee (${feeSats}).`,
|
|
3021
|
+
"INSUFFICIENT_FUNDS",
|
|
3022
|
+
{ bitcoinAddress: opts.beaconAddress, utxoValue: utxo.value, fee: feeSats.toString() }
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
const tx = new import_btc_signer4.Transaction();
|
|
3026
|
+
tx.addInput({
|
|
3027
|
+
txid: utxo.txid,
|
|
3028
|
+
index: utxo.vout,
|
|
3029
|
+
nonWitnessUtxo: prevTxBytes,
|
|
3030
|
+
witnessUtxo: { amount: BigInt(utxo.value), script: witnessScript },
|
|
3031
|
+
tapInternalKey: opts.internalPubkey
|
|
3032
|
+
});
|
|
3033
|
+
tx.addOutputAddress(opts.beaconAddress, BigInt(utxo.value) - feeSats, opts.network);
|
|
3034
|
+
tx.addOutput({ script: opReturnScript(opts.signalBytes), amount: 0n });
|
|
3035
|
+
return {
|
|
3036
|
+
tx,
|
|
3037
|
+
prevOutScripts: [witnessScript],
|
|
3038
|
+
prevOutValues: [BigInt(utxo.value)],
|
|
3039
|
+
beaconAddress: opts.beaconAddress,
|
|
3040
|
+
utxo,
|
|
3041
|
+
feeSats
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
2291
3044
|
var Beacon = class {
|
|
2292
3045
|
/**
|
|
2293
3046
|
* The Beacon service configuration parsed from the DID Document.
|
|
@@ -2297,79 +3050,108 @@ var Beacon = class {
|
|
|
2297
3050
|
this.service = service;
|
|
2298
3051
|
}
|
|
2299
3052
|
/**
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
* Steps:
|
|
2303
|
-
* 1. Parse the beacon's `serviceEndpoint` (stripping `bitcoin:` prefix) into a Bitcoin address.
|
|
2304
|
-
* 2. Query the address for unconfirmed/confirmed UTXOs.
|
|
2305
|
-
* 3. Select the most recent confirmed UTXO.
|
|
2306
|
-
* 4. Fetch the previous transaction hex for `nonWitnessUtxo`.
|
|
2307
|
-
* 5. Build a PSBT: input (UTXO) → change output + OP_RETURN(signalBytes).
|
|
2308
|
-
* 6. Compute the fee via the supplied (or default) {@link FeeEstimator} against the tx vsize.
|
|
2309
|
-
* 7. Sign input 0 with an ECDSA signer derived from `secretKey`.
|
|
2310
|
-
* 8. Finalize, extract, and broadcast via the REST transaction endpoint.
|
|
3053
|
+
* Build + sign + broadcast a single-party beacon signal transaction (P2WPKH spend).
|
|
2311
3054
|
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
3055
|
+
* Composed from the three extracted phases ({@link buildSinglePartyTx},
|
|
3056
|
+
* {@link signSinglePartyTx}, {@link broadcastRawTx}) so each piece can be exercised
|
|
3057
|
+
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead —
|
|
3058
|
+
* the multi-party path can't share the signing phase, but the tx-construction
|
|
3059
|
+
* plumbing (UTXO fetch + OP_RETURN output + change output) is shared.
|
|
2316
3060
|
*
|
|
2317
3061
|
* @param signalBytes 32-byte payload to embed in OP_RETURN.
|
|
2318
3062
|
* @param secretKey Secret key used to sign the spending input.
|
|
2319
3063
|
* @param bitcoin Bitcoin network connection.
|
|
2320
3064
|
* @param options Broadcast options (fee estimator, etc.).
|
|
2321
3065
|
* @returns The txid of the broadcast transaction.
|
|
2322
|
-
* @throws {BeaconError} if the address is unfunded
|
|
3066
|
+
* @throws {BeaconError} if the address is unfunded, no UTXO is available, or fee exceeds value.
|
|
2323
3067
|
*/
|
|
2324
3068
|
async buildSignAndBroadcast(signalBytes, secretKey, bitcoin, options) {
|
|
2325
3069
|
const feeEstimator = options?.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
2326
|
-
const
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3070
|
+
const beaconAddress = this.service.serviceEndpoint.replace("bitcoin:", "");
|
|
3071
|
+
const { utxo, prevTxBytes } = await fetchSpendableUtxo(beaconAddress, bitcoin);
|
|
3072
|
+
const plan = await this.buildSinglePartyTx({
|
|
3073
|
+
signalBytes,
|
|
3074
|
+
beaconAddress,
|
|
3075
|
+
utxo,
|
|
3076
|
+
prevTxBytes,
|
|
3077
|
+
secretKey,
|
|
3078
|
+
bitcoin,
|
|
3079
|
+
feeEstimator
|
|
3080
|
+
});
|
|
3081
|
+
const signedHex = this.signSinglePartyTx(plan.tx, secretKey);
|
|
3082
|
+
return this.broadcastRawTx(bitcoin, signedHex);
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Build an unsigned P2WPKH single-party beacon tx + probe-sign to determine vsize,
|
|
3086
|
+
* then rebuild with the real fee. Returns the tx and prev-output metadata.
|
|
3087
|
+
*
|
|
3088
|
+
* The secret key is required here (not just in `signSinglePartyTx`) because the
|
|
3089
|
+
* two-pass fee estimation requires an actual signature to measure vsize accurately.
|
|
3090
|
+
*/
|
|
3091
|
+
async buildSinglePartyTx(opts) {
|
|
3092
|
+
const pubkey = this.#derivePubkey(opts.secretKey);
|
|
3093
|
+
const witnessOut = (0, import_btc_signer4.p2wpkh)(pubkey, opts.bitcoin.data);
|
|
3094
|
+
const witnessScript = witnessOut.script;
|
|
3095
|
+
const build = (feeSats2) => {
|
|
3096
|
+
const tx2 = new import_btc_signer4.Transaction();
|
|
3097
|
+
tx2.addInput({
|
|
3098
|
+
txid: opts.utxo.txid,
|
|
3099
|
+
index: opts.utxo.vout,
|
|
3100
|
+
nonWitnessUtxo: opts.prevTxBytes,
|
|
3101
|
+
witnessUtxo: { amount: BigInt(opts.utxo.value), script: witnessScript }
|
|
3102
|
+
});
|
|
3103
|
+
tx2.addOutputAddress(
|
|
3104
|
+
opts.beaconAddress,
|
|
3105
|
+
BigInt(opts.utxo.value) - feeSats2,
|
|
3106
|
+
opts.bitcoin.data
|
|
2343
3107
|
);
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
const keyPair = import_keypair2.SchnorrKeyPair.fromSecret(secretKey);
|
|
2347
|
-
const signer = {
|
|
2348
|
-
publicKey: keyPair.publicKey.compressed,
|
|
2349
|
-
sign: (hash5) => keyPair.secretKey.sign(hash5, { scheme: "ecdsa" })
|
|
3108
|
+
tx2.addOutput({ script: opReturnScript(opts.signalBytes), amount: 0n });
|
|
3109
|
+
return tx2;
|
|
2350
3110
|
};
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
const vsize = probeTx.virtualSize();
|
|
2358
|
-
const fee = await feeEstimator.estimateFee(vsize);
|
|
2359
|
-
if (BigInt(utxo.value) <= fee) {
|
|
3111
|
+
const probe = build(0n);
|
|
3112
|
+
probe.signIdx(opts.secretKey, 0);
|
|
3113
|
+
probe.finalize();
|
|
3114
|
+
const vsize = probe.vsize;
|
|
3115
|
+
const feeSats = await opts.feeEstimator.estimateFee(vsize);
|
|
3116
|
+
if (BigInt(opts.utxo.value) <= feeSats) {
|
|
2360
3117
|
throw new BeaconError(
|
|
2361
|
-
`UTXO value (${utxo.value}) insufficient to cover fee (${
|
|
3118
|
+
`UTXO value (${opts.utxo.value}) insufficient to cover fee (${feeSats}).`,
|
|
2362
3119
|
"INSUFFICIENT_FUNDS",
|
|
2363
|
-
{ bitcoinAddress, utxoValue: utxo.value, fee:
|
|
3120
|
+
{ bitcoinAddress: opts.beaconAddress, utxoValue: opts.utxo.value, fee: feeSats.toString() }
|
|
2364
3121
|
);
|
|
2365
3122
|
}
|
|
2366
|
-
const
|
|
2367
|
-
return
|
|
3123
|
+
const tx = build(feeSats);
|
|
3124
|
+
return {
|
|
3125
|
+
tx,
|
|
3126
|
+
prevOutScripts: [witnessScript],
|
|
3127
|
+
prevOutValues: [BigInt(opts.utxo.value)],
|
|
3128
|
+
beaconAddress: opts.beaconAddress,
|
|
3129
|
+
utxo: opts.utxo,
|
|
3130
|
+
feeSats
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Sign + finalize the unsigned single-party tx and return its raw hex.
|
|
3135
|
+
*/
|
|
3136
|
+
signSinglePartyTx(tx, secretKey) {
|
|
3137
|
+
tx.signIdx(secretKey, 0);
|
|
3138
|
+
tx.finalize();
|
|
3139
|
+
return tx.hex;
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Broadcast raw transaction hex via the Bitcoin REST endpoint. Returns the txid.
|
|
3143
|
+
*/
|
|
3144
|
+
async broadcastRawTx(bitcoin, rawHex) {
|
|
3145
|
+
return bitcoin.rest.transaction.send(rawHex);
|
|
3146
|
+
}
|
|
3147
|
+
/** Derive the compressed secp256k1 public key from a raw secret key. */
|
|
3148
|
+
#derivePubkey(secretKey) {
|
|
3149
|
+
return (0, import_secp256k12.getPublicKey)(secretKey, true);
|
|
2368
3150
|
}
|
|
2369
3151
|
};
|
|
2370
3152
|
|
|
2371
3153
|
// src/core/beacon/cas-beacon.ts
|
|
2372
|
-
var
|
|
3154
|
+
var import_common10 = require("@did-btcr2/common");
|
|
2373
3155
|
var CASBeacon = class extends Beacon {
|
|
2374
3156
|
/**
|
|
2375
3157
|
* Creates an instance of CASBeacon.
|
|
@@ -2410,7 +3192,7 @@ var CASBeacon = class extends Beacon {
|
|
|
2410
3192
|
if (!updateHashEncoded) {
|
|
2411
3193
|
continue;
|
|
2412
3194
|
}
|
|
2413
|
-
const updateHash = (0,
|
|
3195
|
+
const updateHash = (0, import_common10.encode)((0, import_common10.decode)(updateHashEncoded, "base64urlnopad"), "hex");
|
|
2414
3196
|
const signedUpdate = sidecar.updateMap.get(updateHash);
|
|
2415
3197
|
if (!signedUpdate) {
|
|
2416
3198
|
needs.push({
|
|
@@ -2442,9 +3224,9 @@ var CASBeacon = class extends Beacon {
|
|
|
2442
3224
|
*/
|
|
2443
3225
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2444
3226
|
const did = this.service.id.split("#")[0];
|
|
2445
|
-
const updateHash = (0,
|
|
3227
|
+
const updateHash = (0, import_common10.canonicalHash)(signedUpdate);
|
|
2446
3228
|
const casAnnouncement = { [did]: updateHash };
|
|
2447
|
-
const announcementHash = (0,
|
|
3229
|
+
const announcementHash = (0, import_common10.hash)((0, import_common10.canonicalize)(casAnnouncement));
|
|
2448
3230
|
await this.buildSignAndBroadcast(announcementHash, secretKey, bitcoin, options);
|
|
2449
3231
|
if (options?.casPublish) {
|
|
2450
3232
|
await options.casPublish(casAnnouncement);
|
|
@@ -2454,10 +3236,10 @@ var CASBeacon = class extends Beacon {
|
|
|
2454
3236
|
};
|
|
2455
3237
|
|
|
2456
3238
|
// src/core/beacon/factory.ts
|
|
2457
|
-
var
|
|
3239
|
+
var import_common13 = require("@did-btcr2/common");
|
|
2458
3240
|
|
|
2459
3241
|
// src/core/beacon/singleton-beacon.ts
|
|
2460
|
-
var
|
|
3242
|
+
var import_common11 = require("@did-btcr2/common");
|
|
2461
3243
|
var SingletonBeacon = class extends Beacon {
|
|
2462
3244
|
/**
|
|
2463
3245
|
* Creates an instance of SingletonBeacon.
|
|
@@ -2505,14 +3287,14 @@ var SingletonBeacon = class extends Beacon {
|
|
|
2505
3287
|
* @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
|
|
2506
3288
|
*/
|
|
2507
3289
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2508
|
-
const signalBytes = (0,
|
|
3290
|
+
const signalBytes = (0, import_common11.hash)((0, import_common11.canonicalize)(signedUpdate));
|
|
2509
3291
|
await this.buildSignAndBroadcast(signalBytes, secretKey, bitcoin, options);
|
|
2510
3292
|
return signedUpdate;
|
|
2511
3293
|
}
|
|
2512
3294
|
};
|
|
2513
3295
|
|
|
2514
3296
|
// src/core/beacon/smt-beacon.ts
|
|
2515
|
-
var
|
|
3297
|
+
var import_common12 = require("@did-btcr2/common");
|
|
2516
3298
|
var import_smt3 = require("@did-btcr2/smt");
|
|
2517
3299
|
var import_utils6 = require("@noble/hashes/utils");
|
|
2518
3300
|
var SMTBeacon = class extends Beacon {
|
|
@@ -2600,7 +3382,7 @@ var SMTBeacon = class extends Beacon {
|
|
|
2600
3382
|
*/
|
|
2601
3383
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2602
3384
|
const did = this.service.id.split("#")[0];
|
|
2603
|
-
const canonicalBytes = new TextEncoder().encode((0,
|
|
3385
|
+
const canonicalBytes = new TextEncoder().encode((0, import_common12.canonicalize)(signedUpdate));
|
|
2604
3386
|
const nonce = (0, import_utils6.randomBytes)(32);
|
|
2605
3387
|
const tree = new import_smt3.BTCR2MerkleTree();
|
|
2606
3388
|
tree.addEntries([{ did, nonce, signedUpdate: canonicalBytes }]);
|
|
@@ -2626,19 +3408,19 @@ var BeaconFactory = class {
|
|
|
2626
3408
|
case "SMTBeacon":
|
|
2627
3409
|
return new SMTBeacon(service);
|
|
2628
3410
|
default:
|
|
2629
|
-
throw new
|
|
3411
|
+
throw new import_common13.MethodError("Invalid Beacon Type", "INVALID_BEACON_ERROR", service);
|
|
2630
3412
|
}
|
|
2631
3413
|
}
|
|
2632
3414
|
};
|
|
2633
3415
|
|
|
2634
3416
|
// src/core/beacon/signal-discovery.ts
|
|
2635
3417
|
var import_bitcoin2 = require("@did-btcr2/bitcoin");
|
|
2636
|
-
var
|
|
3418
|
+
var import_common16 = require("@did-btcr2/common");
|
|
2637
3419
|
|
|
2638
3420
|
// src/core/beacon/utils.ts
|
|
2639
3421
|
var import_bitcoin = require("@did-btcr2/bitcoin");
|
|
2640
|
-
var
|
|
2641
|
-
var
|
|
3422
|
+
var import_common15 = require("@did-btcr2/common");
|
|
3423
|
+
var import_btc_signer5 = require("@scure/btc-signer");
|
|
2642
3424
|
|
|
2643
3425
|
// src/utils/appendix.ts
|
|
2644
3426
|
var import_dids = require("@web5/dids");
|
|
@@ -3679,9 +4461,9 @@ var Appendix = class _Appendix {
|
|
|
3679
4461
|
};
|
|
3680
4462
|
|
|
3681
4463
|
// src/core/identifier.ts
|
|
3682
|
-
var
|
|
3683
|
-
var
|
|
3684
|
-
var
|
|
4464
|
+
var import_common14 = require("@did-btcr2/common");
|
|
4465
|
+
var import_keypair2 = require("@did-btcr2/keypair");
|
|
4466
|
+
var import_base7 = require("@scure/base");
|
|
3685
4467
|
var Identifier = class _Identifier {
|
|
3686
4468
|
/**
|
|
3687
4469
|
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-encoding | 3.2 did:btcr2 Identifier Encoding}.
|
|
@@ -3699,25 +4481,25 @@ var Identifier = class _Identifier {
|
|
|
3699
4481
|
*/
|
|
3700
4482
|
static encode(genesisBytes, options) {
|
|
3701
4483
|
const { idType, version = 1, network } = options;
|
|
3702
|
-
if (!(idType in
|
|
3703
|
-
throw new
|
|
4484
|
+
if (!(idType in import_common14.IdentifierTypes)) {
|
|
4485
|
+
throw new import_common14.IdentifierError('Expected "idType" to be "KEY" or "EXTERNAL"', import_common14.INVALID_DID, { idType });
|
|
3704
4486
|
}
|
|
3705
4487
|
if (isNaN(version) || version > 1) {
|
|
3706
|
-
throw new
|
|
4488
|
+
throw new import_common14.IdentifierError('Expected "version" to be 1', import_common14.INVALID_DID, { version });
|
|
3707
4489
|
}
|
|
3708
|
-
if (typeof network === "string" && !(network in
|
|
3709
|
-
throw new
|
|
4490
|
+
if (typeof network === "string" && !(network in import_common14.BitcoinNetworkNames)) {
|
|
4491
|
+
throw new import_common14.IdentifierError('Invalid "network" name', import_common14.INVALID_DID, { network });
|
|
3710
4492
|
}
|
|
3711
4493
|
if (typeof network === "number" && (network < 0 || network > 8)) {
|
|
3712
|
-
throw new
|
|
4494
|
+
throw new import_common14.IdentifierError('Invalid "network" number', import_common14.INVALID_DID, { network });
|
|
3713
4495
|
}
|
|
3714
4496
|
if (idType === "KEY") {
|
|
3715
4497
|
try {
|
|
3716
|
-
new
|
|
4498
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3717
4499
|
} catch {
|
|
3718
|
-
throw new
|
|
4500
|
+
throw new import_common14.IdentifierError(
|
|
3719
4501
|
'Expected "genesisBytes" to be a valid compressed secp256k1 public key',
|
|
3720
|
-
|
|
4502
|
+
import_common14.INVALID_DID,
|
|
3721
4503
|
{ genesisBytes }
|
|
3722
4504
|
);
|
|
3723
4505
|
}
|
|
@@ -3730,7 +4512,7 @@ var Identifier = class _Identifier {
|
|
|
3730
4512
|
}
|
|
3731
4513
|
nibbles.push((version - 1) % 15);
|
|
3732
4514
|
if (typeof network === "string") {
|
|
3733
|
-
nibbles.push(
|
|
4515
|
+
nibbles.push(import_common14.BitcoinNetworkNames[network]);
|
|
3734
4516
|
} else if (typeof network === "number") {
|
|
3735
4517
|
nibbles.push(network + 11);
|
|
3736
4518
|
}
|
|
@@ -3739,11 +4521,11 @@ var Identifier = class _Identifier {
|
|
|
3739
4521
|
}
|
|
3740
4522
|
if (fCount !== 0) {
|
|
3741
4523
|
for (const index in Array.from({ length: nibbles.length / 2 - 1 })) {
|
|
3742
|
-
throw new
|
|
4524
|
+
throw new import_common14.IdentifierError("Not implemented", "NOT_IMPLEMENTED", { index });
|
|
3743
4525
|
}
|
|
3744
4526
|
}
|
|
3745
4527
|
const dataBytes = new Uint8Array([nibbles[2 * 0] << 4 | nibbles[2 * 0 + 1], ...genesisBytes]);
|
|
3746
|
-
return `did:btcr2:${
|
|
4528
|
+
return `did:btcr2:${import_base7.bech32m.encodeFromBytes(hrp, dataBytes)}`;
|
|
3747
4529
|
}
|
|
3748
4530
|
/**
|
|
3749
4531
|
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-decoding | 3.3 did:btcr2 Identifier Decoding}.
|
|
@@ -3756,24 +4538,24 @@ var Identifier = class _Identifier {
|
|
|
3756
4538
|
static decode(identifier) {
|
|
3757
4539
|
const components = identifier.split(":");
|
|
3758
4540
|
if (components.length !== 3) {
|
|
3759
|
-
throw new
|
|
4541
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3760
4542
|
}
|
|
3761
4543
|
const [scheme, method, encoded] = components;
|
|
3762
4544
|
if (scheme !== "did") {
|
|
3763
|
-
throw new
|
|
4545
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3764
4546
|
}
|
|
3765
4547
|
if (method !== "btcr2") {
|
|
3766
|
-
throw new
|
|
4548
|
+
throw new import_common14.IdentifierError(`Invalid did method: ${method}`, import_common14.METHOD_NOT_SUPPORTED, { identifier });
|
|
3767
4549
|
}
|
|
3768
4550
|
if (!encoded) {
|
|
3769
|
-
throw new
|
|
4551
|
+
throw new import_common14.IdentifierError(`Invalid method-specific id: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3770
4552
|
}
|
|
3771
|
-
const { prefix: hrp, bytes: dataBytes } =
|
|
4553
|
+
const { prefix: hrp, bytes: dataBytes } = import_base7.bech32m.decodeToBytes(encoded);
|
|
3772
4554
|
if (!["x", "k"].includes(hrp)) {
|
|
3773
|
-
throw new
|
|
4555
|
+
throw new import_common14.IdentifierError(`Invalid hrp: ${hrp}`, import_common14.INVALID_DID, { identifier });
|
|
3774
4556
|
}
|
|
3775
4557
|
if (!dataBytes) {
|
|
3776
|
-
throw new
|
|
4558
|
+
throw new import_common14.IdentifierError(`Failed to decode id: ${encoded}`, import_common14.INVALID_DID, { identifier });
|
|
3777
4559
|
}
|
|
3778
4560
|
const idType = hrp === "k" ? "KEY" : "EXTERNAL";
|
|
3779
4561
|
let version = 1;
|
|
@@ -3791,33 +4573,33 @@ var Identifier = class _Identifier {
|
|
|
3791
4573
|
}
|
|
3792
4574
|
nibblesConsumed += 1;
|
|
3793
4575
|
if (version > 1) {
|
|
3794
|
-
throw new
|
|
4576
|
+
throw new import_common14.IdentifierError(`Invalid version: ${version}`, import_common14.INVALID_DID, { identifier });
|
|
3795
4577
|
}
|
|
3796
4578
|
}
|
|
3797
4579
|
version += versionNibble;
|
|
3798
4580
|
nibblesConsumed += 1;
|
|
3799
4581
|
let networkValue = nibblesConsumed % 2 === 0 ? dataBytes[++byteIndex] >>> 4 : currentByte & 15;
|
|
3800
4582
|
nibblesConsumed += 1;
|
|
3801
|
-
let network =
|
|
4583
|
+
let network = import_common14.BitcoinNetworkNames[networkValue];
|
|
3802
4584
|
if (!network) {
|
|
3803
4585
|
if (networkValue >= 8 && networkValue <= 15) {
|
|
3804
4586
|
network = networkValue - 11;
|
|
3805
4587
|
} else {
|
|
3806
|
-
throw new
|
|
4588
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3807
4589
|
}
|
|
3808
4590
|
}
|
|
3809
4591
|
if (nibblesConsumed % 2 === 1) {
|
|
3810
4592
|
const fillerNibble = currentByte & 15;
|
|
3811
4593
|
if (fillerNibble !== 0) {
|
|
3812
|
-
throw new
|
|
4594
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3813
4595
|
}
|
|
3814
4596
|
}
|
|
3815
4597
|
const genesisBytes = dataBytes.slice(byteIndex + 1);
|
|
3816
4598
|
if (idType === "KEY") {
|
|
3817
4599
|
try {
|
|
3818
|
-
new
|
|
4600
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3819
4601
|
} catch {
|
|
3820
|
-
throw new
|
|
4602
|
+
throw new import_common14.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common14.INVALID_DID, { identifier });
|
|
3821
4603
|
}
|
|
3822
4604
|
}
|
|
3823
4605
|
return { idType, hrp, version, network, genesisBytes };
|
|
@@ -3827,7 +4609,7 @@ var Identifier = class _Identifier {
|
|
|
3827
4609
|
* @returns {string} The new did:btcr2 identifier.
|
|
3828
4610
|
*/
|
|
3829
4611
|
static generate() {
|
|
3830
|
-
const keyPair =
|
|
4612
|
+
const keyPair = import_keypair2.SchnorrKeyPair.generate();
|
|
3831
4613
|
const did = this.encode(
|
|
3832
4614
|
keyPair.publicKey.compressed,
|
|
3833
4615
|
{
|
|
@@ -3847,13 +4629,13 @@ var Identifier = class _Identifier {
|
|
|
3847
4629
|
static getPublicKey(did) {
|
|
3848
4630
|
const { idType, genesisBytes } = _Identifier.decode(did);
|
|
3849
4631
|
if (idType !== "KEY") {
|
|
3850
|
-
throw new
|
|
4632
|
+
throw new import_common14.IdentifierError(
|
|
3851
4633
|
`Cannot extract public key from EXTERNAL DID: ${did}. EXTERNAL DIDs encode a document hash, not a public key.`,
|
|
3852
|
-
|
|
4634
|
+
import_common14.INVALID_DID,
|
|
3853
4635
|
{ did, idType }
|
|
3854
4636
|
);
|
|
3855
4637
|
}
|
|
3856
|
-
return new
|
|
4638
|
+
return new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3857
4639
|
}
|
|
3858
4640
|
/**
|
|
3859
4641
|
* Validates a did:btcr2 identifier.
|
|
@@ -3880,7 +4662,7 @@ var BeaconUtils = class {
|
|
|
3880
4662
|
*/
|
|
3881
4663
|
static parseBitcoinAddress(uri) {
|
|
3882
4664
|
if (!uri.startsWith("bitcoin:")) {
|
|
3883
|
-
throw new
|
|
4665
|
+
throw new import_common15.MethodError("Invalid Bitcoin URI format", "BEACON_SERVICE_ERROR", { uri });
|
|
3884
4666
|
}
|
|
3885
4667
|
return uri.replace("bitcoin:", "").split("?")[0];
|
|
3886
4668
|
}
|
|
@@ -3938,7 +4720,8 @@ var BeaconUtils = class {
|
|
|
3938
4720
|
const network = (0, import_bitcoin.getNetwork)(components.network);
|
|
3939
4721
|
const pubkey = components.genesisBytes;
|
|
3940
4722
|
const id = `${did}#initial${addressType.toUpperCase()}`;
|
|
3941
|
-
const
|
|
4723
|
+
const address = addressType === "p2tr" ? (0, import_btc_signer5.p2tr)(pubkey.slice(1, 33), void 0, network).address : addressType === "p2wpkh" ? (0, import_btc_signer5.p2wpkh)(pubkey, network).address : (0, import_btc_signer5.p2pkh)(pubkey, network).address;
|
|
4724
|
+
const serviceEndpoint = `bitcoin:${address}`;
|
|
3942
4725
|
return { id, type: beaconType, serviceEndpoint };
|
|
3943
4726
|
} catch (error) {
|
|
3944
4727
|
throw new BeaconError(
|
|
@@ -3956,27 +4739,27 @@ var BeaconUtils = class {
|
|
|
3956
4739
|
*/
|
|
3957
4740
|
static generateBeaconServices({ id, publicKey, network, beaconType }) {
|
|
3958
4741
|
try {
|
|
3959
|
-
const
|
|
3960
|
-
const
|
|
3961
|
-
const
|
|
3962
|
-
if (!
|
|
3963
|
-
throw new
|
|
4742
|
+
const p2pkhAddr = (0, import_btc_signer5.p2pkh)(publicKey, network).address;
|
|
4743
|
+
const p2wpkhAddr = (0, import_btc_signer5.p2wpkh)(publicKey, network).address;
|
|
4744
|
+
const p2trAddr = (0, import_btc_signer5.p2tr)(publicKey.slice(1, 33), void 0, network).address;
|
|
4745
|
+
if (!p2pkhAddr || !p2wpkhAddr || !p2trAddr) {
|
|
4746
|
+
throw new import_common15.DidMethodError("Failed to generate bitcoin addresses");
|
|
3964
4747
|
}
|
|
3965
4748
|
return [
|
|
3966
4749
|
{
|
|
3967
4750
|
id: `${id}#initialP2PKH`,
|
|
3968
4751
|
type: beaconType,
|
|
3969
|
-
serviceEndpoint: `bitcoin:${
|
|
4752
|
+
serviceEndpoint: `bitcoin:${p2pkhAddr}`
|
|
3970
4753
|
},
|
|
3971
4754
|
{
|
|
3972
4755
|
id: `${id}#initialP2WPKH`,
|
|
3973
4756
|
type: beaconType,
|
|
3974
|
-
serviceEndpoint: `bitcoin:${
|
|
4757
|
+
serviceEndpoint: `bitcoin:${p2wpkhAddr}`
|
|
3975
4758
|
},
|
|
3976
4759
|
{
|
|
3977
4760
|
id: `${id}#initialP2TR`,
|
|
3978
4761
|
type: beaconType,
|
|
3979
|
-
serviceEndpoint: `bitcoin:${
|
|
4762
|
+
serviceEndpoint: `bitcoin:${p2trAddr}`
|
|
3980
4763
|
}
|
|
3981
4764
|
];
|
|
3982
4765
|
} catch (error) {
|
|
@@ -4076,7 +4859,7 @@ var BeaconSignalDiscovery = class {
|
|
|
4076
4859
|
}
|
|
4077
4860
|
const rpc = bitcoin.rpc;
|
|
4078
4861
|
if (!rpc) {
|
|
4079
|
-
throw new
|
|
4862
|
+
throw new import_common16.ResolveError("RPC connection is not available", "RPC_CONNECTION_ERROR", bitcoin);
|
|
4080
4863
|
}
|
|
4081
4864
|
const targetHeight = await rpc.getBlockCount();
|
|
4082
4865
|
const beaconServicesMap = BeaconUtils.getBeaconServicesMap(beaconServices);
|
|
@@ -4147,27 +4930,24 @@ var BeaconSignalDiscovery = class {
|
|
|
4147
4930
|
|
|
4148
4931
|
// src/core/resolver.ts
|
|
4149
4932
|
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
4150
|
-
var
|
|
4151
|
-
var
|
|
4152
|
-
var
|
|
4933
|
+
var import_common20 = require("@did-btcr2/common");
|
|
4934
|
+
var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
|
|
4935
|
+
var import_keypair4 = require("@did-btcr2/keypair");
|
|
4153
4936
|
|
|
4154
4937
|
// src/did-btcr2.ts
|
|
4155
|
-
var
|
|
4938
|
+
var import_common19 = require("@did-btcr2/common");
|
|
4156
4939
|
var import_dids2 = require("@web5/dids");
|
|
4157
|
-
var ecc = __toESM(require("@bitcoinerlab/secp256k1"), 1);
|
|
4158
|
-
var import_utils9 = require("@noble/hashes/utils");
|
|
4159
|
-
var import_bitcoinjs_lib7 = require("bitcoinjs-lib");
|
|
4160
4940
|
|
|
4161
|
-
// src/core/
|
|
4162
|
-
var
|
|
4163
|
-
var
|
|
4941
|
+
// src/core/updater.ts
|
|
4942
|
+
var import_common18 = require("@did-btcr2/common");
|
|
4943
|
+
var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
|
|
4164
4944
|
|
|
4165
4945
|
// src/utils/did-document.ts
|
|
4166
4946
|
var import_bitcoin3 = require("@did-btcr2/bitcoin");
|
|
4167
|
-
var
|
|
4168
|
-
var
|
|
4947
|
+
var import_common17 = require("@did-btcr2/common");
|
|
4948
|
+
var import_keypair3 = require("@did-btcr2/keypair");
|
|
4169
4949
|
var import_utils8 = require("@web5/dids/utils");
|
|
4170
|
-
var
|
|
4950
|
+
var import_btc_signer6 = require("@scure/btc-signer");
|
|
4171
4951
|
var BTCR2_DID_DOCUMENT_CONTEXT = [
|
|
4172
4952
|
"https://www.w3.org/ns/did/v1.1",
|
|
4173
4953
|
"https://btcr2.dev/context/v1"
|
|
@@ -4208,20 +4988,20 @@ var DidDocument = class _DidDocument {
|
|
|
4208
4988
|
deactivated;
|
|
4209
4989
|
constructor(document) {
|
|
4210
4990
|
if (!document.id) {
|
|
4211
|
-
throw new
|
|
4991
|
+
throw new import_common17.DidDocumentError("DID Document must have an id", import_common17.INVALID_DID_DOCUMENT, document);
|
|
4212
4992
|
}
|
|
4213
|
-
const idType = document.id.includes("k1") ?
|
|
4993
|
+
const idType = document.id.includes("k1") ? import_common17.IdentifierTypes.KEY : import_common17.IdentifierTypes.EXTERNAL;
|
|
4214
4994
|
const isGenesis = document.id === ID_PLACEHOLDER_VALUE;
|
|
4215
4995
|
const { id, verificationMethod: vm, service } = document;
|
|
4216
4996
|
if (!isGenesis) {
|
|
4217
4997
|
if (!_DidDocument.isValidId(id)) {
|
|
4218
|
-
throw new
|
|
4998
|
+
throw new import_common17.DidDocumentError(`Invalid id: ${id}`, import_common17.INVALID_DID_DOCUMENT, document);
|
|
4219
4999
|
}
|
|
4220
5000
|
if (!_DidDocument.isValidVerificationMethods(vm)) {
|
|
4221
|
-
throw new
|
|
5001
|
+
throw new import_common17.DidDocumentError("Invalid verificationMethod: " + vm, import_common17.INVALID_DID_DOCUMENT, document);
|
|
4222
5002
|
}
|
|
4223
5003
|
if (!_DidDocument.isValidServices(service)) {
|
|
4224
|
-
throw new
|
|
5004
|
+
throw new import_common17.DidDocumentError("Invalid service: " + service, import_common17.INVALID_DID_DOCUMENT, document);
|
|
4225
5005
|
}
|
|
4226
5006
|
}
|
|
4227
5007
|
this.id = document.id;
|
|
@@ -4231,7 +5011,7 @@ var DidDocument = class _DidDocument {
|
|
|
4231
5011
|
"https://www.w3.org/ns/did/v1.1",
|
|
4232
5012
|
"https://btcr2.dev/context/v1"
|
|
4233
5013
|
];
|
|
4234
|
-
if (idType ===
|
|
5014
|
+
if (idType === import_common17.IdentifierTypes.KEY) {
|
|
4235
5015
|
const keyRef = `${this.id}#initialKey`;
|
|
4236
5016
|
this.authentication = document.authentication || [keyRef];
|
|
4237
5017
|
this.assertionMethod = document.assertionMethod || [keyRef];
|
|
@@ -4317,19 +5097,19 @@ var DidDocument = class _DidDocument {
|
|
|
4317
5097
|
*/
|
|
4318
5098
|
static isValid(didDocument) {
|
|
4319
5099
|
if (!this.isValidContext(didDocument?.["@context"])) {
|
|
4320
|
-
throw new
|
|
5100
|
+
throw new import_common17.DidDocumentError('Invalid "@context"', import_common17.INVALID_DID_DOCUMENT, didDocument);
|
|
4321
5101
|
}
|
|
4322
5102
|
if (!this.isValidId(didDocument?.id)) {
|
|
4323
|
-
throw new
|
|
5103
|
+
throw new import_common17.DidDocumentError('Invalid "id"', import_common17.INVALID_DID_DOCUMENT, didDocument);
|
|
4324
5104
|
}
|
|
4325
5105
|
if (!this.isValidVerificationMethods(didDocument?.verificationMethod)) {
|
|
4326
|
-
throw new
|
|
5106
|
+
throw new import_common17.DidDocumentError('Invalid "verificationMethod"', import_common17.INVALID_DID_DOCUMENT, didDocument);
|
|
4327
5107
|
}
|
|
4328
5108
|
if (!this.isValidServices(didDocument?.service)) {
|
|
4329
|
-
throw new
|
|
5109
|
+
throw new import_common17.DidDocumentError('Invalid "service"', import_common17.INVALID_DID_DOCUMENT, didDocument);
|
|
4330
5110
|
}
|
|
4331
5111
|
if (!this.isValidVerificationRelationships(didDocument)) {
|
|
4332
|
-
throw new
|
|
5112
|
+
throw new import_common17.DidDocumentError("Invalid verification relationships", import_common17.INVALID_DID_DOCUMENT, didDocument);
|
|
4333
5113
|
}
|
|
4334
5114
|
return true;
|
|
4335
5115
|
}
|
|
@@ -4419,16 +5199,16 @@ var DidDocument = class _DidDocument {
|
|
|
4419
5199
|
*/
|
|
4420
5200
|
validateGenesis() {
|
|
4421
5201
|
if (this.id !== ID_PLACEHOLDER_VALUE) {
|
|
4422
|
-
throw new
|
|
5202
|
+
throw new import_common17.DidDocumentError("Invalid GenesisDocument ID", import_common17.INVALID_DID_DOCUMENT, this);
|
|
4423
5203
|
}
|
|
4424
5204
|
if (!this.verificationMethod.every((vm) => vm.id.includes(ID_PLACEHOLDER_VALUE) && vm.controller === ID_PLACEHOLDER_VALUE)) {
|
|
4425
|
-
throw new
|
|
5205
|
+
throw new import_common17.DidDocumentError("Invalid GenesisDocument verificationMethod", import_common17.INVALID_DID_DOCUMENT, this);
|
|
4426
5206
|
}
|
|
4427
5207
|
if (!this.service.every((svc) => svc.id.includes(ID_PLACEHOLDER_VALUE))) {
|
|
4428
|
-
throw new
|
|
5208
|
+
throw new import_common17.DidDocumentError("Invalid GenesisDocument service", import_common17.INVALID_DID_DOCUMENT, this);
|
|
4429
5209
|
}
|
|
4430
5210
|
if (!_DidDocument.isValidVerificationRelationships(this)) {
|
|
4431
|
-
throw new
|
|
5211
|
+
throw new import_common17.DidDocumentError("Invalid GenesisDocument assertionMethod", import_common17.INVALID_DID_DOCUMENT, this);
|
|
4432
5212
|
}
|
|
4433
5213
|
return true;
|
|
4434
5214
|
}
|
|
@@ -4438,7 +5218,7 @@ var DidDocument = class _DidDocument {
|
|
|
4438
5218
|
*/
|
|
4439
5219
|
toIntermediate() {
|
|
4440
5220
|
if (this.id.includes("k1")) {
|
|
4441
|
-
throw new
|
|
5221
|
+
throw new import_common17.DidDocumentError("Cannot convert a key identifier to an intermediate document", import_common17.INVALID_DID_DOCUMENT, this);
|
|
4442
5222
|
}
|
|
4443
5223
|
return new GenesisDocument(this);
|
|
4444
5224
|
}
|
|
@@ -4468,7 +5248,7 @@ var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
|
4468
5248
|
* @returns {GenesisDocument} The GenesisDocument representation of the DidDocument.
|
|
4469
5249
|
*/
|
|
4470
5250
|
static fromDidDocument(didDocument) {
|
|
4471
|
-
const intermediateDocument =
|
|
5251
|
+
const intermediateDocument = import_common17.JSONUtils.cloneReplace(didDocument, DID_REGEX, ID_PLACEHOLDER_VALUE);
|
|
4472
5252
|
return new _GenesisDocument(intermediateDocument);
|
|
4473
5253
|
}
|
|
4474
5254
|
/**
|
|
@@ -4487,9 +5267,9 @@ var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
|
4487
5267
|
* @returns {GenesisDocument} A new GenesisDocument with the placeholder ID.
|
|
4488
5268
|
*/
|
|
4489
5269
|
static fromPublicKey(publicKey, network) {
|
|
4490
|
-
const pk = new
|
|
5270
|
+
const pk = new import_keypair3.CompressedSecp256k1PublicKey(publicKey);
|
|
4491
5271
|
const id = ID_PLACEHOLDER_VALUE;
|
|
4492
|
-
const address =
|
|
5272
|
+
const address = (0, import_btc_signer6.p2pkh)(pk.compressed, (0, import_bitcoin3.getNetwork)(network)).address;
|
|
4493
5273
|
const services = [{
|
|
4494
5274
|
id: `${id}#service-0`,
|
|
4495
5275
|
serviceEndpoint: `bitcoin:${address}`,
|
|
@@ -4525,21 +5305,41 @@ var GenesisDocument = class _GenesisDocument extends DidDocument {
|
|
|
4525
5305
|
* @returns {Bytes} The genesis bytes.
|
|
4526
5306
|
*/
|
|
4527
5307
|
static toGenesisBytes(genesisDocument) {
|
|
4528
|
-
return (0,
|
|
5308
|
+
return (0, import_common17.hash)((0, import_common17.canonicalize)(genesisDocument));
|
|
4529
5309
|
}
|
|
4530
5310
|
};
|
|
4531
5311
|
|
|
4532
|
-
// src/core/
|
|
4533
|
-
var
|
|
5312
|
+
// src/core/updater.ts
|
|
5313
|
+
var Updater = class _Updater {
|
|
5314
|
+
#phase = "Construct" /* Construct */;
|
|
5315
|
+
#sourceDocument;
|
|
5316
|
+
#patches;
|
|
5317
|
+
#sourceVersionId;
|
|
5318
|
+
#verificationMethod;
|
|
5319
|
+
#beaconService;
|
|
5320
|
+
#unsignedUpdate = null;
|
|
5321
|
+
#signedUpdate = null;
|
|
5322
|
+
/**
|
|
5323
|
+
* @internal — Use {@link DidBtcr2.update} to create instances.
|
|
5324
|
+
*/
|
|
5325
|
+
constructor(params) {
|
|
5326
|
+
this.#sourceDocument = params.sourceDocument;
|
|
5327
|
+
this.#patches = params.patches;
|
|
5328
|
+
this.#sourceVersionId = params.sourceVersionId;
|
|
5329
|
+
this.#verificationMethod = params.verificationMethod;
|
|
5330
|
+
this.#beaconService = params.beaconService;
|
|
5331
|
+
}
|
|
5332
|
+
// ─── Public static utility methods ─────────────────────────────────────────
|
|
5333
|
+
// Used by generate-vector.ts and other scripts that need direct access to
|
|
5334
|
+
// individual update steps outside the state machine flow.
|
|
4534
5335
|
/**
|
|
4535
5336
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-unsigned-update | 7.3.b Construct BTCR2 Unsigned Update}.
|
|
4536
|
-
* This process constructs a BTCR2 Unsigned Update conformant to the spec template.
|
|
4537
5337
|
*
|
|
4538
5338
|
* @param {Btcr2DidDocument} sourceDocument The source DID document to be updated.
|
|
4539
|
-
* @param {PatchOperation[]} patches The
|
|
5339
|
+
* @param {PatchOperation[]} patches The JSON Patch operations to apply.
|
|
4540
5340
|
* @param {number} sourceVersionId The version ID of the source document.
|
|
4541
5341
|
* @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
|
|
4542
|
-
* @throws {UpdateError}
|
|
5342
|
+
* @throws {UpdateError} If the target document fails DID Core validation.
|
|
4543
5343
|
*/
|
|
4544
5344
|
static construct(sourceDocument, patches, sourceVersionId) {
|
|
4545
5345
|
const unsignedUpdate = {
|
|
@@ -4552,35 +5352,34 @@ var Update = class {
|
|
|
4552
5352
|
patch: patches,
|
|
4553
5353
|
targetHash: "",
|
|
4554
5354
|
targetVersionId: sourceVersionId + 1,
|
|
4555
|
-
sourceHash: (0,
|
|
5355
|
+
sourceHash: (0, import_common18.canonicalHash)(sourceDocument)
|
|
4556
5356
|
};
|
|
4557
|
-
const targetDocument =
|
|
5357
|
+
const targetDocument = import_common18.JSONPatch.apply(sourceDocument, patches);
|
|
4558
5358
|
try {
|
|
4559
5359
|
DidDocument.isValid(targetDocument);
|
|
4560
5360
|
} catch (error) {
|
|
4561
|
-
throw new
|
|
5361
|
+
throw new import_common18.UpdateError(
|
|
4562
5362
|
"Error validating targetDocument: " + (error instanceof Error ? error.message : String(error)),
|
|
4563
|
-
|
|
5363
|
+
import_common18.INVALID_DID_UPDATE,
|
|
4564
5364
|
targetDocument
|
|
4565
5365
|
);
|
|
4566
5366
|
}
|
|
4567
|
-
unsignedUpdate.targetHash = (0,
|
|
5367
|
+
unsignedUpdate.targetHash = (0, import_common18.canonicalHash)(targetDocument);
|
|
4568
5368
|
return unsignedUpdate;
|
|
4569
5369
|
}
|
|
4570
5370
|
/**
|
|
4571
5371
|
* Implements subsection {@link http://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-signed-update | 7.3.c Construct BTCR2 Signed Update }.
|
|
4572
|
-
* This process constructs a BTCR2 Signed Update from a BTCR2 Unsigned Update.
|
|
4573
5372
|
*
|
|
4574
|
-
* @param {string} did The did-btcr2 identifier to derive the root capability from
|
|
4575
|
-
* @param {UnsignedBTCR2Update} unsignedUpdate The
|
|
4576
|
-
* @param {DidVerificationMethod} verificationMethod The
|
|
4577
|
-
* @
|
|
4578
|
-
* @
|
|
5373
|
+
* @param {string} did The did-btcr2 identifier to derive the root capability from.
|
|
5374
|
+
* @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
|
|
5375
|
+
* @param {DidVerificationMethod} verificationMethod The verification method for signing.
|
|
5376
|
+
* @param {KeyBytes} secretKey The secret key bytes.
|
|
5377
|
+
* @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
|
|
4579
5378
|
*/
|
|
4580
5379
|
static sign(did, unsignedUpdate, verificationMethod, secretKey) {
|
|
4581
5380
|
const controller = verificationMethod.controller;
|
|
4582
5381
|
const id = verificationMethod.id.slice(verificationMethod.id.indexOf("#"));
|
|
4583
|
-
const multikey =
|
|
5382
|
+
const multikey = import_cryptosuite2.SchnorrMultikey.fromSecretKey(id, controller, secretKey);
|
|
4584
5383
|
const config = {
|
|
4585
5384
|
"@context": [
|
|
4586
5385
|
"https://w3id.org/security/v2",
|
|
@@ -4600,22 +5399,145 @@ var Update = class {
|
|
|
4600
5399
|
}
|
|
4601
5400
|
/**
|
|
4602
5401
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
|
|
4603
|
-
*
|
|
4604
|
-
*
|
|
4605
|
-
* @param {
|
|
4606
|
-
* @param {
|
|
4607
|
-
* @
|
|
4608
|
-
* @
|
|
5402
|
+
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
5403
|
+
*
|
|
5404
|
+
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
5405
|
+
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
5406
|
+
* @param {KeyBytes} secretKey The secret key for signing the Bitcoin transaction.
|
|
5407
|
+
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
5408
|
+
* @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
|
|
4609
5409
|
*/
|
|
4610
5410
|
static async announce(beaconService, update, secretKey, bitcoin) {
|
|
4611
5411
|
const beacon = BeaconFactory.establish(beaconService);
|
|
4612
|
-
|
|
4613
|
-
|
|
5412
|
+
return beacon.broadcastSignal(update, secretKey, bitcoin);
|
|
5413
|
+
}
|
|
5414
|
+
// ─── Private instance wrappers ─────────────────────────────────────────────
|
|
5415
|
+
// Delegate to the public statics with bound instance fields for cleaner
|
|
5416
|
+
// advance/provide code.
|
|
5417
|
+
#construct() {
|
|
5418
|
+
return _Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
|
|
5419
|
+
}
|
|
5420
|
+
#sign(secretKey) {
|
|
5421
|
+
return _Updater.sign(this.#sourceDocument.id, this.#unsignedUpdate, this.#verificationMethod, secretKey);
|
|
5422
|
+
}
|
|
5423
|
+
// ─── State machine ─────────────────────────────────────────────────────────
|
|
5424
|
+
/**
|
|
5425
|
+
* Advance the state machine. Returns either:
|
|
5426
|
+
* - `{ status: 'action-required', needs }` — caller must provide data via {@link provide}
|
|
5427
|
+
* - `{ status: 'complete', result }` — update is signed and broadcast
|
|
5428
|
+
*/
|
|
5429
|
+
advance() {
|
|
5430
|
+
while (true) {
|
|
5431
|
+
switch (this.#phase) {
|
|
5432
|
+
// Phase: Construct
|
|
5433
|
+
// Build the unsigned update from source doc + patches. Pure, synchronous.
|
|
5434
|
+
case "Construct" /* Construct */: {
|
|
5435
|
+
this.#unsignedUpdate = this.#construct();
|
|
5436
|
+
this.#phase = "Sign" /* Sign */;
|
|
5437
|
+
continue;
|
|
5438
|
+
}
|
|
5439
|
+
// Phase: Sign
|
|
5440
|
+
// Emit NeedSigningKey — the caller supplies the secret key (or a KMS signature).
|
|
5441
|
+
case "Sign" /* Sign */: {
|
|
5442
|
+
return {
|
|
5443
|
+
status: "action-required",
|
|
5444
|
+
needs: [{
|
|
5445
|
+
kind: "NeedSigningKey",
|
|
5446
|
+
verificationMethodId: this.#verificationMethod.id,
|
|
5447
|
+
unsignedUpdate: this.#unsignedUpdate
|
|
5448
|
+
}]
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5451
|
+
// Phase: Fund
|
|
5452
|
+
// Emit NeedFunding with the beacon address. The caller checks UTXOs,
|
|
5453
|
+
// funds the address if needed, and provides to continue.
|
|
5454
|
+
case "Fund" /* Fund */: {
|
|
5455
|
+
const beaconAddress = this.#beaconService.serviceEndpoint.replace("bitcoin:", "");
|
|
5456
|
+
return {
|
|
5457
|
+
status: "action-required",
|
|
5458
|
+
needs: [{
|
|
5459
|
+
kind: "NeedFunding",
|
|
5460
|
+
beaconAddress,
|
|
5461
|
+
beaconService: this.#beaconService
|
|
5462
|
+
}]
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
// Phase: Broadcast
|
|
5466
|
+
// Emit NeedBroadcast with the signed update + beacon service. The caller performs
|
|
5467
|
+
// the actual on-chain announcement (or hands off to the aggregation protocol).
|
|
5468
|
+
case "Broadcast" /* Broadcast */: {
|
|
5469
|
+
return {
|
|
5470
|
+
status: "action-required",
|
|
5471
|
+
needs: [{
|
|
5472
|
+
kind: "NeedBroadcast",
|
|
5473
|
+
beaconService: this.#beaconService,
|
|
5474
|
+
signedUpdate: this.#signedUpdate
|
|
5475
|
+
}]
|
|
5476
|
+
};
|
|
5477
|
+
}
|
|
5478
|
+
// Phase: Complete
|
|
5479
|
+
case "Complete" /* Complete */: {
|
|
5480
|
+
return {
|
|
5481
|
+
status: "complete",
|
|
5482
|
+
result: { signedUpdate: this.#signedUpdate }
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
provide(need, data) {
|
|
5489
|
+
switch (need.kind) {
|
|
5490
|
+
case "NeedSigningKey": {
|
|
5491
|
+
if (this.#phase !== "Sign" /* Sign */) {
|
|
5492
|
+
throw new import_common18.UpdateError(
|
|
5493
|
+
`Cannot provide NeedSigningKey: updater phase is ${this.#phase}, expected Sign.`,
|
|
5494
|
+
import_common18.INVALID_DID_UPDATE,
|
|
5495
|
+
{ phase: this.#phase }
|
|
5496
|
+
);
|
|
5497
|
+
}
|
|
5498
|
+
if (!data) {
|
|
5499
|
+
throw new import_common18.UpdateError(
|
|
5500
|
+
"NeedSigningKey requires secret key bytes.",
|
|
5501
|
+
import_common18.INVALID_DID_UPDATE
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5504
|
+
if (!this.#unsignedUpdate) {
|
|
5505
|
+
throw new import_common18.UpdateError(
|
|
5506
|
+
"Internal error: unsigned update missing in Sign phase.",
|
|
5507
|
+
import_common18.INVALID_DID_UPDATE
|
|
5508
|
+
);
|
|
5509
|
+
}
|
|
5510
|
+
this.#signedUpdate = this.#sign(data);
|
|
5511
|
+
this.#phase = "Fund" /* Fund */;
|
|
5512
|
+
break;
|
|
5513
|
+
}
|
|
5514
|
+
case "NeedFunding": {
|
|
5515
|
+
if (this.#phase !== "Fund" /* Fund */) {
|
|
5516
|
+
throw new import_common18.UpdateError(
|
|
5517
|
+
`Cannot provide NeedFunding: updater phase is ${this.#phase}, expected Fund.`,
|
|
5518
|
+
import_common18.INVALID_DID_UPDATE,
|
|
5519
|
+
{ phase: this.#phase }
|
|
5520
|
+
);
|
|
5521
|
+
}
|
|
5522
|
+
this.#phase = "Broadcast" /* Broadcast */;
|
|
5523
|
+
break;
|
|
5524
|
+
}
|
|
5525
|
+
case "NeedBroadcast": {
|
|
5526
|
+
if (this.#phase !== "Broadcast" /* Broadcast */) {
|
|
5527
|
+
throw new import_common18.UpdateError(
|
|
5528
|
+
`Cannot provide NeedBroadcast: updater phase is ${this.#phase}, expected Broadcast.`,
|
|
5529
|
+
import_common18.INVALID_DID_UPDATE,
|
|
5530
|
+
{ phase: this.#phase }
|
|
5531
|
+
);
|
|
5532
|
+
}
|
|
5533
|
+
this.#phase = "Complete" /* Complete */;
|
|
5534
|
+
break;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
4614
5537
|
}
|
|
4615
5538
|
};
|
|
4616
5539
|
|
|
4617
5540
|
// src/did-btcr2.ts
|
|
4618
|
-
(0, import_bitcoinjs_lib7.initEccLib)(ecc);
|
|
4619
5541
|
var DidBtcr2 = class {
|
|
4620
5542
|
/**
|
|
4621
5543
|
* Name of the DID method, as defined in the DID BTCR2 specification
|
|
@@ -4640,9 +5562,9 @@ var DidBtcr2 = class {
|
|
|
4640
5562
|
static create(genesisBytes, options) {
|
|
4641
5563
|
const { idType, version = 1, network = "bitcoin" } = options || {};
|
|
4642
5564
|
if (!idType) {
|
|
4643
|
-
throw new
|
|
5565
|
+
throw new import_common19.MethodError(
|
|
4644
5566
|
"idType is required for creating a did:btcr2 identifier",
|
|
4645
|
-
|
|
5567
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4646
5568
|
options
|
|
4647
5569
|
);
|
|
4648
5570
|
}
|
|
@@ -4672,7 +5594,7 @@ var DidBtcr2 = class {
|
|
|
4672
5594
|
static resolve(did, resolutionOptions = {}) {
|
|
4673
5595
|
const didComponents = Identifier.decode(did);
|
|
4674
5596
|
const sidecarData = Resolver.sidecarData(resolutionOptions.sidecar);
|
|
4675
|
-
const currentDocument = didComponents.hrp ===
|
|
5597
|
+
const currentDocument = didComponents.hrp === import_common19.IdentifierHrp.k ? Resolver.deterministic(didComponents) : null;
|
|
4676
5598
|
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
4677
5599
|
versionId: resolutionOptions.versionId,
|
|
4678
5600
|
versionTime: resolutionOptions.versionTime,
|
|
@@ -4680,90 +5602,79 @@ var DidBtcr2 = class {
|
|
|
4680
5602
|
});
|
|
4681
5603
|
}
|
|
4682
5604
|
/**
|
|
4683
|
-
* Entry point for section {@link https://dcdpr.github.io/did-btcr2/#
|
|
4684
|
-
*
|
|
4685
|
-
*
|
|
5605
|
+
* Entry point for section {@link https://dcdpr.github.io/did-btcr2/#update | 7.3 Update}.
|
|
5606
|
+
*
|
|
5607
|
+
* Factory method that validates the update parameters and returns a sans-I/O
|
|
5608
|
+
* {@link Updater} state machine. The caller drives the updater through its
|
|
5609
|
+
* phases (Construct → Sign → Broadcast → Complete) by calling `advance()` and
|
|
5610
|
+
* `provide()`. The method package performs **zero I/O** — signing key retrieval
|
|
5611
|
+
* (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
|
|
5612
|
+
*
|
|
5613
|
+
* For a fully-wired version with Bitcoin broadcast and key handling, see
|
|
5614
|
+
* `DidMethodApi.update()` in `@did-btcr2/api`.
|
|
4686
5615
|
*
|
|
4687
|
-
*
|
|
4688
|
-
* published to the Bitcoin network. Any property in the DID document may be updated except the id. Doing so would
|
|
4689
|
-
* invalidate the DID document.
|
|
4690
|
-
* @param params An object containing the parameters for the update operation.
|
|
5616
|
+
* @param params Update construction parameters.
|
|
4691
5617
|
* @param {Btcr2DidDocument} params.sourceDocument The DID document being updated.
|
|
4692
|
-
* @param {PatchOperation[]} params.patches The
|
|
4693
|
-
* @param {
|
|
4694
|
-
* @param {string} params.verificationMethodId The
|
|
4695
|
-
* @param {string} params.beaconId The beacon ID
|
|
4696
|
-
* @
|
|
4697
|
-
* @
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
static async update({
|
|
5618
|
+
* @param {PatchOperation[]} params.patches The JSON Patch operations to apply.
|
|
5619
|
+
* @param {number} params.sourceVersionId The version ID before applying the update.
|
|
5620
|
+
* @param {string} params.verificationMethodId The verification method ID to sign with.
|
|
5621
|
+
* @param {string} params.beaconId The beacon service ID to broadcast through.
|
|
5622
|
+
* @returns {Updater} A sans-I/O state machine for driving the update.
|
|
5623
|
+
* @throws {UpdateError} If the verification method is not authorized, not found,
|
|
5624
|
+
* not of type `Multikey`, or does not have a `zQ3s` publicKeyMultibase prefix.
|
|
5625
|
+
* Also throws if the beacon service is not found.
|
|
5626
|
+
*/
|
|
5627
|
+
static update({
|
|
4703
5628
|
sourceDocument,
|
|
4704
5629
|
patches,
|
|
4705
5630
|
sourceVersionId,
|
|
4706
5631
|
verificationMethodId,
|
|
4707
|
-
beaconId
|
|
4708
|
-
signingMaterial,
|
|
4709
|
-
bitcoin
|
|
5632
|
+
beaconId
|
|
4710
5633
|
}) {
|
|
4711
|
-
if (!signingMaterial) {
|
|
4712
|
-
throw new import_common17.UpdateError(
|
|
4713
|
-
"Missing signing material for update",
|
|
4714
|
-
import_common17.INVALID_DID_UPDATE,
|
|
4715
|
-
{ signingMaterial }
|
|
4716
|
-
);
|
|
4717
|
-
}
|
|
4718
|
-
const secretKey = typeof signingMaterial === "string" ? (0, import_utils9.hexToBytes)(signingMaterial) : signingMaterial;
|
|
4719
5634
|
if (!sourceDocument.capabilityInvocation?.some((vr) => vr === verificationMethodId)) {
|
|
4720
|
-
throw new
|
|
5635
|
+
throw new import_common19.UpdateError(
|
|
4721
5636
|
"Invalid verificationMethodId: not authorized for capabilityInvocation",
|
|
4722
|
-
|
|
5637
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4723
5638
|
sourceDocument
|
|
4724
5639
|
);
|
|
4725
5640
|
}
|
|
4726
5641
|
const verificationMethod = this.getSigningMethod(sourceDocument, verificationMethodId);
|
|
4727
5642
|
if (!verificationMethod) {
|
|
4728
|
-
throw new
|
|
5643
|
+
throw new import_common19.UpdateError(
|
|
4729
5644
|
"Invalid verificationMethod: not found in source document",
|
|
4730
|
-
|
|
5645
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4731
5646
|
{ sourceDocument, verificationMethodId }
|
|
4732
5647
|
);
|
|
4733
5648
|
}
|
|
4734
5649
|
if (verificationMethod.type !== "Multikey") {
|
|
4735
|
-
throw new
|
|
5650
|
+
throw new import_common19.UpdateError(
|
|
4736
5651
|
'Invalid verificationMethod: verificationMethod.type must be "Multikey"',
|
|
4737
|
-
|
|
5652
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4738
5653
|
verificationMethod
|
|
4739
5654
|
);
|
|
4740
5655
|
}
|
|
4741
5656
|
if (verificationMethod.publicKeyMultibase?.slice(0, 4) !== "zQ3s") {
|
|
4742
|
-
throw new
|
|
5657
|
+
throw new import_common19.UpdateError(
|
|
4743
5658
|
'Invalid verificationMethodId: publicKeyMultibase prefix must start with "zQ3s"',
|
|
4744
|
-
|
|
5659
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4745
5660
|
verificationMethod
|
|
4746
5661
|
);
|
|
4747
5662
|
}
|
|
4748
|
-
const update = Update.construct(sourceDocument, patches, sourceVersionId);
|
|
4749
|
-
const signed = Update.sign(sourceDocument.id, update, verificationMethod, secretKey);
|
|
4750
5663
|
const beaconService = sourceDocument.service.filter((service) => service.id === beaconId).filter((service) => !!service).shift();
|
|
4751
5664
|
if (!beaconService) {
|
|
4752
|
-
throw new
|
|
5665
|
+
throw new import_common19.UpdateError(
|
|
4753
5666
|
"No beacon service found for provided beaconId",
|
|
4754
|
-
|
|
5667
|
+
import_common19.INVALID_DID_UPDATE,
|
|
4755
5668
|
{ sourceDocument, beaconId }
|
|
4756
5669
|
);
|
|
4757
5670
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
}
|
|
4765
|
-
await Update.announce(beaconService, signed, secretKey, bitcoin);
|
|
4766
|
-
return signed;
|
|
5671
|
+
return new Updater({
|
|
5672
|
+
sourceDocument,
|
|
5673
|
+
patches,
|
|
5674
|
+
sourceVersionId,
|
|
5675
|
+
verificationMethod,
|
|
5676
|
+
beaconService
|
|
5677
|
+
});
|
|
4767
5678
|
}
|
|
4768
5679
|
/**
|
|
4769
5680
|
* Given the W3C DID Document of a `did:btcr2` identifier, return the signing verification method that will be used
|
|
@@ -4779,7 +5690,7 @@ var DidBtcr2 = class {
|
|
|
4779
5690
|
methodId ??= "#initialKey";
|
|
4780
5691
|
const parsedDid = import_dids2.Did.parse(didDocument.id);
|
|
4781
5692
|
if (parsedDid && parsedDid.method !== this.methodName) {
|
|
4782
|
-
throw new
|
|
5693
|
+
throw new import_common19.MethodError(`Method not supported: ${parsedDid.method}`, import_common19.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
|
|
4783
5694
|
}
|
|
4784
5695
|
const verificationMethod = didDocument.verificationMethod?.find(
|
|
4785
5696
|
(vm) => Appendix.extractDidFragment(vm.id) === (Appendix.extractDidFragment(methodId) ?? Appendix.extractDidFragment(didDocument.assertionMethod?.[0]))
|
|
@@ -4795,7 +5706,7 @@ var DidBtcr2 = class {
|
|
|
4795
5706
|
};
|
|
4796
5707
|
|
|
4797
5708
|
// src/core/resolver.ts
|
|
4798
|
-
var
|
|
5709
|
+
var import_utils10 = require("@noble/curves/utils.js");
|
|
4799
5710
|
var Resolver = class _Resolver {
|
|
4800
5711
|
// --- Immutable inputs ---
|
|
4801
5712
|
#didComponents;
|
|
@@ -4835,7 +5746,7 @@ var Resolver = class _Resolver {
|
|
|
4835
5746
|
static deterministic(didComponents) {
|
|
4836
5747
|
const genesisBytes = didComponents.genesisBytes;
|
|
4837
5748
|
const did = Identifier.encode(genesisBytes, didComponents);
|
|
4838
|
-
const { multibase } = new
|
|
5749
|
+
const { multibase } = new import_keypair4.CompressedSecp256k1PublicKey(genesisBytes);
|
|
4839
5750
|
const service = BeaconUtils.generateBeaconServices({
|
|
4840
5751
|
id: did,
|
|
4841
5752
|
publicKey: genesisBytes,
|
|
@@ -4861,14 +5772,14 @@ var Resolver = class _Resolver {
|
|
|
4861
5772
|
* @throws {ResolveError} InvalidDidDocument if not conformant to DID Core v1.1
|
|
4862
5773
|
*/
|
|
4863
5774
|
static external(didComponents, genesisDocument) {
|
|
4864
|
-
const genesisDocumentHash = (0,
|
|
4865
|
-
if (!(0,
|
|
4866
|
-
throw new
|
|
5775
|
+
const genesisDocumentHash = (0, import_common20.canonicalHashBytes)(genesisDocument);
|
|
5776
|
+
if (!(0, import_utils10.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
|
|
5777
|
+
throw new import_common20.ResolveError(
|
|
4867
5778
|
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
4868
|
-
|
|
5779
|
+
import_common20.INVALID_DID_DOCUMENT,
|
|
4869
5780
|
{
|
|
4870
|
-
genesisBytes: (0,
|
|
4871
|
-
genesisDocumentHash: (0,
|
|
5781
|
+
genesisBytes: (0, import_common20.encode)(didComponents.genesisBytes, "hex"),
|
|
5782
|
+
genesisDocumentHash: (0, import_common20.encode)(genesisDocumentHash, "hex")
|
|
4872
5783
|
}
|
|
4873
5784
|
);
|
|
4874
5785
|
}
|
|
@@ -4887,12 +5798,12 @@ var Resolver = class _Resolver {
|
|
|
4887
5798
|
const updateMap = /* @__PURE__ */ new Map();
|
|
4888
5799
|
if (sidecar.updates?.length)
|
|
4889
5800
|
for (const update of sidecar.updates) {
|
|
4890
|
-
updateMap.set((0,
|
|
5801
|
+
updateMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
4891
5802
|
}
|
|
4892
5803
|
const casMap = /* @__PURE__ */ new Map();
|
|
4893
5804
|
if (sidecar.casUpdates?.length)
|
|
4894
5805
|
for (const update of sidecar.casUpdates) {
|
|
4895
|
-
casMap.set((0,
|
|
5806
|
+
casMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
4896
5807
|
}
|
|
4897
5808
|
const smtMap = /* @__PURE__ */ new Map();
|
|
4898
5809
|
if (sidecar.smtProofs?.length)
|
|
@@ -4925,12 +5836,12 @@ var Resolver = class _Resolver {
|
|
|
4925
5836
|
}
|
|
4926
5837
|
};
|
|
4927
5838
|
for (const [update, block] of updates) {
|
|
4928
|
-
const currentDocumentHash = (0,
|
|
4929
|
-
const blocktime =
|
|
4930
|
-
response.metadata.updated =
|
|
5839
|
+
const currentDocumentHash = (0, import_common20.canonicalHashBytes)(response.didDocument);
|
|
5840
|
+
const blocktime = import_common20.DateUtils.blocktimeToTimestamp(block.time);
|
|
5841
|
+
response.metadata.updated = import_common20.DateUtils.toISOStringNonFractional(blocktime);
|
|
4931
5842
|
response.metadata.confirmations = block.confirmations;
|
|
4932
5843
|
if (versionTime) {
|
|
4933
|
-
if (blocktime >
|
|
5844
|
+
if (blocktime > import_common20.DateUtils.dateStringToTimestamp(versionTime)) {
|
|
4934
5845
|
return response;
|
|
4935
5846
|
}
|
|
4936
5847
|
}
|
|
@@ -4938,22 +5849,22 @@ var Resolver = class _Resolver {
|
|
|
4938
5849
|
updateHashHistory.push(currentDocumentHash);
|
|
4939
5850
|
this.confirmDuplicate(update, updateHashHistory);
|
|
4940
5851
|
} else if (update.targetVersionId === currentVersionId + 1) {
|
|
4941
|
-
const sourceHashBytes = (0,
|
|
4942
|
-
if (!(0,
|
|
4943
|
-
throw new
|
|
5852
|
+
const sourceHashBytes = (0, import_common20.decode)(update.sourceHash, "base64urlnopad");
|
|
5853
|
+
if (!(0, import_utils10.equalBytes)(sourceHashBytes, currentDocumentHash)) {
|
|
5854
|
+
throw new import_common20.ResolveError(
|
|
4944
5855
|
`Hash mismatch: update.sourceHash !== currentDocumentHash`,
|
|
4945
|
-
|
|
5856
|
+
import_common20.INVALID_DID_UPDATE,
|
|
4946
5857
|
{
|
|
4947
5858
|
sourceHash: update.sourceHash,
|
|
4948
|
-
currentDocumentHash: (0,
|
|
5859
|
+
currentDocumentHash: (0, import_common20.encode)(currentDocumentHash, "hex")
|
|
4949
5860
|
}
|
|
4950
5861
|
);
|
|
4951
5862
|
}
|
|
4952
5863
|
response.didDocument = this.applyUpdate(response.didDocument, update);
|
|
4953
|
-
const unsignedUpdate =
|
|
4954
|
-
updateHashHistory.push((0,
|
|
5864
|
+
const unsignedUpdate = import_common20.JSONUtils.deleteKeys(update, ["proof"]);
|
|
5865
|
+
updateHashHistory.push((0, import_common20.canonicalHashBytes)(unsignedUpdate));
|
|
4955
5866
|
} else if (update.targetVersionId > currentVersionId + 1) {
|
|
4956
|
-
throw new
|
|
5867
|
+
throw new import_common20.ResolveError(
|
|
4957
5868
|
`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
|
|
4958
5869
|
"LATE_PUBLISHING_ERROR",
|
|
4959
5870
|
{
|
|
@@ -4984,15 +5895,15 @@ var Resolver = class _Resolver {
|
|
|
4984
5895
|
*/
|
|
4985
5896
|
static confirmDuplicate(update, updateHashHistory) {
|
|
4986
5897
|
const { proof: _, ...unsignedUpdate } = update;
|
|
4987
|
-
const unsignedUpdateHash = (0,
|
|
5898
|
+
const unsignedUpdateHash = (0, import_common20.canonicalHashBytes)(unsignedUpdate);
|
|
4988
5899
|
const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
|
|
4989
|
-
if (!(0,
|
|
4990
|
-
throw new
|
|
5900
|
+
if (!(0, import_utils10.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
|
|
5901
|
+
throw new import_common20.ResolveError(
|
|
4991
5902
|
`Invalid duplicate: unsigned update hash does not match historical hash`,
|
|
4992
|
-
|
|
5903
|
+
import_common20.LATE_PUBLISHING_ERROR,
|
|
4993
5904
|
{
|
|
4994
|
-
unsignedUpdateHash: (0,
|
|
4995
|
-
historicalHash: (0,
|
|
5905
|
+
unsignedUpdateHash: (0, import_common20.encode)(unsignedUpdateHash, "hex"),
|
|
5906
|
+
historicalHash: (0, import_common20.encode)(historicalUpdateHash, "hex")
|
|
4996
5907
|
}
|
|
4997
5908
|
);
|
|
4998
5909
|
}
|
|
@@ -5007,42 +5918,42 @@ var Resolver = class _Resolver {
|
|
|
5007
5918
|
static applyUpdate(currentDocument, update) {
|
|
5008
5919
|
const capabilityId = update.proof?.capability;
|
|
5009
5920
|
if (!capabilityId) {
|
|
5010
|
-
throw new
|
|
5921
|
+
throw new import_common20.ResolveError("No root capability found in update", import_common20.INVALID_DID_UPDATE, update);
|
|
5011
5922
|
}
|
|
5012
5923
|
const rootCapability = Appendix.dereferenceZcapId(capabilityId);
|
|
5013
5924
|
const { invocationTarget, controller: rootController } = rootCapability;
|
|
5014
5925
|
if (![invocationTarget, rootController].every((id) => id === currentDocument.id)) {
|
|
5015
|
-
throw new
|
|
5926
|
+
throw new import_common20.ResolveError(
|
|
5016
5927
|
"Invalid root capability",
|
|
5017
|
-
|
|
5928
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5018
5929
|
{ rootCapability, currentDocument }
|
|
5019
5930
|
);
|
|
5020
5931
|
}
|
|
5021
5932
|
const verificationMethodId = update.proof?.verificationMethod;
|
|
5022
5933
|
if (!verificationMethodId) {
|
|
5023
|
-
throw new
|
|
5934
|
+
throw new import_common20.ResolveError("No verificationMethod found in update", import_common20.INVALID_DID_UPDATE, update);
|
|
5024
5935
|
}
|
|
5025
5936
|
const vm = DidBtcr2.getSigningMethod(currentDocument, verificationMethodId);
|
|
5026
|
-
const multikey =
|
|
5027
|
-
const cryptosuite = new
|
|
5028
|
-
const canonicalUpdate = (0,
|
|
5029
|
-
const diProof = new
|
|
5937
|
+
const multikey = import_cryptosuite3.SchnorrMultikey.fromVerificationMethod(vm);
|
|
5938
|
+
const cryptosuite = new import_cryptosuite3.BIP340Cryptosuite(multikey);
|
|
5939
|
+
const canonicalUpdate = (0, import_common20.canonicalize)(update);
|
|
5940
|
+
const diProof = new import_cryptosuite3.BIP340DataIntegrityProof(cryptosuite);
|
|
5030
5941
|
const verificationResult = diProof.verifyProof(canonicalUpdate, "capabilityInvocation");
|
|
5031
5942
|
if (!verificationResult.verified) {
|
|
5032
|
-
throw new
|
|
5943
|
+
throw new import_common20.ResolveError(
|
|
5033
5944
|
"Invalid update: proof not verified",
|
|
5034
|
-
|
|
5945
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5035
5946
|
verificationResult
|
|
5036
5947
|
);
|
|
5037
5948
|
}
|
|
5038
|
-
const updatedDocument =
|
|
5949
|
+
const updatedDocument = import_common20.JSONPatch.apply(currentDocument, update.patch);
|
|
5039
5950
|
DidDocument.validate(updatedDocument);
|
|
5040
|
-
const currentDocumentHash = (0,
|
|
5041
|
-
const updateTargetHash = (0,
|
|
5042
|
-
if (!(0,
|
|
5043
|
-
throw new
|
|
5951
|
+
const currentDocumentHash = (0, import_common20.canonicalHashBytes)(updatedDocument);
|
|
5952
|
+
const updateTargetHash = (0, import_common20.decode)(update.targetHash);
|
|
5953
|
+
if (!(0, import_utils10.equalBytes)(updateTargetHash, currentDocumentHash)) {
|
|
5954
|
+
throw new import_common20.ResolveError(
|
|
5044
5955
|
`Invalid update: update.targetHash !== currentDocumentHash`,
|
|
5045
|
-
|
|
5956
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5046
5957
|
{ updateTargetHash, currentDocumentHash }
|
|
5047
5958
|
);
|
|
5048
5959
|
}
|
|
@@ -5070,7 +5981,7 @@ var Resolver = class _Resolver {
|
|
|
5070
5981
|
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
5071
5982
|
continue;
|
|
5072
5983
|
}
|
|
5073
|
-
const genesisHash = (0,
|
|
5984
|
+
const genesisHash = (0, import_common20.encode)(this.#didComponents.genesisBytes, "hex");
|
|
5074
5985
|
return {
|
|
5075
5986
|
status: "action-required",
|
|
5076
5987
|
needs: [{ kind: "NeedGenesisDocument", genesisHash }]
|
|
@@ -5174,21 +6085,21 @@ var Resolver = class _Resolver {
|
|
|
5174
6085
|
}
|
|
5175
6086
|
case "NeedCASAnnouncement": {
|
|
5176
6087
|
const announcement = data;
|
|
5177
|
-
this.#sidecarData.casMap.set((0,
|
|
6088
|
+
this.#sidecarData.casMap.set((0, import_common20.canonicalHash)(announcement, { encoding: "hex" }), announcement);
|
|
5178
6089
|
break;
|
|
5179
6090
|
}
|
|
5180
6091
|
case "NeedSignedUpdate": {
|
|
5181
6092
|
const update = data;
|
|
5182
|
-
this.#sidecarData.updateMap.set((0,
|
|
6093
|
+
this.#sidecarData.updateMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
5183
6094
|
break;
|
|
5184
6095
|
}
|
|
5185
6096
|
case "NeedSMTProof": {
|
|
5186
6097
|
const smtNeed = need;
|
|
5187
6098
|
const proof = data;
|
|
5188
6099
|
if (proof.id !== smtNeed.smtRootHash) {
|
|
5189
|
-
throw new
|
|
6100
|
+
throw new import_common20.ResolveError(
|
|
5190
6101
|
`SMT proof root hash mismatch: expected ${smtNeed.smtRootHash}, got ${proof.id}`,
|
|
5191
|
-
|
|
6102
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5192
6103
|
{ expected: smtNeed.smtRootHash, actual: proof.id }
|
|
5193
6104
|
);
|
|
5194
6105
|
}
|
|
@@ -5200,12 +6111,12 @@ var Resolver = class _Resolver {
|
|
|
5200
6111
|
};
|
|
5201
6112
|
|
|
5202
6113
|
// src/utils/did-document-builder.ts
|
|
5203
|
-
var
|
|
6114
|
+
var import_common21 = require("@did-btcr2/common");
|
|
5204
6115
|
var DidDocumentBuilder = class {
|
|
5205
6116
|
document = {};
|
|
5206
6117
|
constructor(initialDocument) {
|
|
5207
6118
|
if (!initialDocument.id) {
|
|
5208
|
-
throw new
|
|
6119
|
+
throw new import_common21.DidDocumentError('Missing required "id" property', import_common21.INVALID_DID_DOCUMENT, initialDocument);
|
|
5209
6120
|
}
|
|
5210
6121
|
this.document.id = initialDocument.id;
|
|
5211
6122
|
this.document.verificationMethod = initialDocument.verificationMethod ?? [];
|
|
@@ -5257,6 +6168,7 @@ var DidDocumentBuilder = class {
|
|
|
5257
6168
|
0 && (module.exports = {
|
|
5258
6169
|
AGGREGATED_NONCE,
|
|
5259
6170
|
AGGREGATION_MESSAGE_PREFIX,
|
|
6171
|
+
AGGREGATION_WIRE_VERSION,
|
|
5260
6172
|
AUTHORIZATION_REQUEST,
|
|
5261
6173
|
AggregateBeaconError,
|
|
5262
6174
|
AggregationCohort,
|
|
@@ -5283,6 +6195,10 @@ var DidDocumentBuilder = class {
|
|
|
5283
6195
|
COHORT_OPT_IN,
|
|
5284
6196
|
COHORT_OPT_IN_ACCEPT,
|
|
5285
6197
|
COHORT_READY,
|
|
6198
|
+
CONSOLE_LOGGER,
|
|
6199
|
+
DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
6200
|
+
DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
6201
|
+
DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
5286
6202
|
DEFAULT_NOSTR_RELAYS,
|
|
5287
6203
|
DID_REGEX,
|
|
5288
6204
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
@@ -5300,6 +6216,7 @@ var DidDocumentBuilder = class {
|
|
|
5300
6216
|
ParticipantCohortPhase,
|
|
5301
6217
|
Resolver,
|
|
5302
6218
|
SIGNATURE_AUTHORIZATION,
|
|
6219
|
+
SILENT_LOGGER,
|
|
5303
6220
|
SMTBeacon,
|
|
5304
6221
|
SMTBeaconError,
|
|
5305
6222
|
SUBMIT_UPDATE,
|
|
@@ -5308,12 +6225,14 @@ var DidDocumentBuilder = class {
|
|
|
5308
6225
|
SigningSessionPhase,
|
|
5309
6226
|
SingletonBeacon,
|
|
5310
6227
|
SingletonBeaconError,
|
|
6228
|
+
StaticFeeEstimator,
|
|
5311
6229
|
TransportAdapterError,
|
|
5312
6230
|
TransportError,
|
|
5313
6231
|
TransportFactory,
|
|
5314
6232
|
TypedEventEmitter,
|
|
5315
|
-
|
|
6233
|
+
Updater,
|
|
5316
6234
|
VALIDATION_ACK,
|
|
6235
|
+
buildAggregationBeaconTx,
|
|
5317
6236
|
createAggregatedNonceMessage,
|
|
5318
6237
|
createAuthorizationRequestMessage,
|
|
5319
6238
|
createCohortAdvertMessage,
|
|
@@ -5325,8 +6244,22 @@ var DidDocumentBuilder = class {
|
|
|
5325
6244
|
createSignatureAuthorizationMessage,
|
|
5326
6245
|
createSubmitUpdateMessage,
|
|
5327
6246
|
createValidationAckMessage,
|
|
6247
|
+
getBeaconStrategy,
|
|
6248
|
+
isAggregatedNonceMessage,
|
|
5328
6249
|
isAggregationMessageType,
|
|
6250
|
+
isAuthorizationRequestMessage,
|
|
6251
|
+
isCohortAdvertMessage,
|
|
6252
|
+
isCohortOptInAcceptMessage,
|
|
6253
|
+
isCohortOptInMessage,
|
|
6254
|
+
isCohortReadyMessage,
|
|
6255
|
+
isDistributeAggregatedDataMessage,
|
|
5329
6256
|
isKeygenMessageType,
|
|
6257
|
+
isNonceContributionMessage,
|
|
5330
6258
|
isSignMessageType,
|
|
5331
|
-
|
|
6259
|
+
isSignatureAuthorizationMessage,
|
|
6260
|
+
isSubmitUpdateMessage,
|
|
6261
|
+
isUpdateMessageType,
|
|
6262
|
+
isValidationAckMessage,
|
|
6263
|
+
opReturnScript,
|
|
6264
|
+
registerBeaconStrategy
|
|
5332
6265
|
});
|