@did-btcr2/method 0.28.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/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +20092 -31631
- package/dist/browser.mjs +20019 -31558
- package/dist/cjs/index.js +1164 -364
- 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 +33 -7
- 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/did-btcr2.js +0 -4
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +2 -0
- 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 +7 -5
- 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/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -7
- 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 +41 -7
- 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/did-btcr2.ts +0 -5
- package/src/index.ts +2 -0
- package/src/utils/did-document.ts +2 -2
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,
|
|
@@ -90,6 +96,7 @@ __export(index_exports, {
|
|
|
90
96
|
TypedEventEmitter: () => TypedEventEmitter,
|
|
91
97
|
Updater: () => Updater,
|
|
92
98
|
VALIDATION_ACK: () => VALIDATION_ACK,
|
|
99
|
+
buildAggregationBeaconTx: () => buildAggregationBeaconTx,
|
|
93
100
|
createAggregatedNonceMessage: () => createAggregatedNonceMessage,
|
|
94
101
|
createAuthorizationRequestMessage: () => createAuthorizationRequestMessage,
|
|
95
102
|
createCohortAdvertMessage: () => createCohortAdvertMessage,
|
|
@@ -101,41 +108,114 @@ __export(index_exports, {
|
|
|
101
108
|
createSignatureAuthorizationMessage: () => createSignatureAuthorizationMessage,
|
|
102
109
|
createSubmitUpdateMessage: () => createSubmitUpdateMessage,
|
|
103
110
|
createValidationAckMessage: () => createValidationAckMessage,
|
|
111
|
+
getBeaconStrategy: () => getBeaconStrategy,
|
|
112
|
+
isAggregatedNonceMessage: () => isAggregatedNonceMessage,
|
|
104
113
|
isAggregationMessageType: () => isAggregationMessageType,
|
|
114
|
+
isAuthorizationRequestMessage: () => isAuthorizationRequestMessage,
|
|
115
|
+
isCohortAdvertMessage: () => isCohortAdvertMessage,
|
|
116
|
+
isCohortOptInAcceptMessage: () => isCohortOptInAcceptMessage,
|
|
117
|
+
isCohortOptInMessage: () => isCohortOptInMessage,
|
|
118
|
+
isCohortReadyMessage: () => isCohortReadyMessage,
|
|
119
|
+
isDistributeAggregatedDataMessage: () => isDistributeAggregatedDataMessage,
|
|
105
120
|
isKeygenMessageType: () => isKeygenMessageType,
|
|
121
|
+
isNonceContributionMessage: () => isNonceContributionMessage,
|
|
106
122
|
isSignMessageType: () => isSignMessageType,
|
|
107
|
-
|
|
123
|
+
isSignatureAuthorizationMessage: () => isSignatureAuthorizationMessage,
|
|
124
|
+
isSubmitUpdateMessage: () => isSubmitUpdateMessage,
|
|
125
|
+
isUpdateMessageType: () => isUpdateMessageType,
|
|
126
|
+
isValidationAckMessage: () => isValidationAckMessage,
|
|
127
|
+
opReturnScript: () => opReturnScript,
|
|
128
|
+
registerBeaconStrategy: () => registerBeaconStrategy
|
|
108
129
|
});
|
|
109
130
|
module.exports = __toCommonJS(index_exports);
|
|
110
131
|
|
|
111
132
|
// src/core/aggregation/service.ts
|
|
133
|
+
var import_common4 = require("@did-btcr2/common");
|
|
134
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
112
135
|
var import_utils2 = require("@noble/hashes/utils");
|
|
113
136
|
|
|
114
|
-
// src/core/aggregation/
|
|
115
|
-
var
|
|
137
|
+
// src/core/aggregation/beacon-strategy.ts
|
|
138
|
+
var import_common = require("@did-btcr2/common");
|
|
116
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");
|
|
117
197
|
var import_utils = require("@noble/hashes/utils");
|
|
198
|
+
var import_btc_signer = require("@scure/btc-signer");
|
|
118
199
|
var import_musig2 = require("@scure/btc-signer/musig2");
|
|
119
|
-
var import_bitcoinjs_lib = require("bitcoinjs-lib");
|
|
120
200
|
|
|
121
201
|
// src/core/aggregation/errors.ts
|
|
122
|
-
var
|
|
123
|
-
var AggregationServiceError = class extends
|
|
202
|
+
var import_common2 = require("@did-btcr2/common");
|
|
203
|
+
var AggregationServiceError = class extends import_common2.MethodError {
|
|
124
204
|
constructor(message, type = "AggregationServiceError", data) {
|
|
125
205
|
super(message, type, data);
|
|
126
206
|
}
|
|
127
207
|
};
|
|
128
|
-
var AggregationParticipantError = class extends
|
|
208
|
+
var AggregationParticipantError = class extends import_common2.MethodError {
|
|
129
209
|
constructor(message, type = "AggregationParticipantError", data) {
|
|
130
210
|
super(message, type, data);
|
|
131
211
|
}
|
|
132
212
|
};
|
|
133
|
-
var AggregationCohortError = class extends
|
|
213
|
+
var AggregationCohortError = class extends import_common2.MethodError {
|
|
134
214
|
constructor(message, type = "AggregationCohortError", data) {
|
|
135
215
|
super(message, type, data);
|
|
136
216
|
}
|
|
137
217
|
};
|
|
138
|
-
var SigningSessionError = class extends
|
|
218
|
+
var SigningSessionError = class extends import_common2.MethodError {
|
|
139
219
|
constructor(message, type = "SigningSessionError", data) {
|
|
140
220
|
super(message, type, data);
|
|
141
221
|
}
|
|
@@ -155,10 +235,21 @@ var AggregationCohort = class {
|
|
|
155
235
|
beaconType;
|
|
156
236
|
/** List of participant DIDs that have been accepted into the cohort. */
|
|
157
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();
|
|
158
245
|
/** Sorted list of cohort participants' compressed public keys. */
|
|
159
246
|
#cohortKeys = [];
|
|
160
|
-
/**
|
|
161
|
-
|
|
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();
|
|
162
253
|
/** The n-of-n MuSig2 Taproot beacon address. */
|
|
163
254
|
beaconAddress = "";
|
|
164
255
|
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
@@ -189,7 +280,7 @@ var AggregationCohort = class {
|
|
|
189
280
|
}
|
|
190
281
|
/**
|
|
191
282
|
* Computes the n-of-n MuSig2 Taproot beacon address from cohort keys.
|
|
192
|
-
* Sets `
|
|
283
|
+
* Sets `tapTweak` to the BIP-341 key-path-only tweak.
|
|
193
284
|
*/
|
|
194
285
|
computeBeaconAddress() {
|
|
195
286
|
if (this.#cohortKeys.length === 0) {
|
|
@@ -201,8 +292,8 @@ var AggregationCohort = class {
|
|
|
201
292
|
}
|
|
202
293
|
const keyAggContext = (0, import_musig2.keyAggregate)(this.#cohortKeys);
|
|
203
294
|
const aggPubkey = (0, import_musig2.keyAggExport)(keyAggContext);
|
|
204
|
-
const payment =
|
|
205
|
-
this.
|
|
295
|
+
const payment = (0, import_btc_signer.p2tr)(aggPubkey);
|
|
296
|
+
this.tapTweak = import_secp256k1.schnorr.utils.taggedHash("TapTweak", aggPubkey);
|
|
206
297
|
if (!payment.address) {
|
|
207
298
|
throw new AggregationCohortError(
|
|
208
299
|
"Failed to compute Taproot address",
|
|
@@ -236,6 +327,18 @@ var AggregationCohort = class {
|
|
|
236
327
|
);
|
|
237
328
|
}
|
|
238
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
|
+
}
|
|
239
342
|
addUpdate(participantDid, signedUpdate) {
|
|
240
343
|
if (!this.participants.includes(participantDid)) {
|
|
241
344
|
throw new AggregationCohortError(
|
|
@@ -264,10 +367,10 @@ var AggregationCohort = class {
|
|
|
264
367
|
}
|
|
265
368
|
const announcement = {};
|
|
266
369
|
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
267
|
-
announcement[did] = (0,
|
|
370
|
+
announcement[did] = (0, import_common3.canonicalHash)(signedUpdate);
|
|
268
371
|
}
|
|
269
372
|
this.casAnnouncement = announcement;
|
|
270
|
-
this.signalBytes = (0,
|
|
373
|
+
this.signalBytes = (0, import_common3.hash)((0, import_common3.canonicalize)(announcement));
|
|
271
374
|
return announcement;
|
|
272
375
|
}
|
|
273
376
|
/**
|
|
@@ -283,11 +386,11 @@ var AggregationCohort = class {
|
|
|
283
386
|
{ cohortId: this.id }
|
|
284
387
|
);
|
|
285
388
|
}
|
|
286
|
-
const tree = new
|
|
389
|
+
const tree = new import_smt2.BTCR2MerkleTree();
|
|
287
390
|
const entries = [];
|
|
288
391
|
const encoder = new TextEncoder();
|
|
289
392
|
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
290
|
-
const canonicalBytes = encoder.encode((0,
|
|
393
|
+
const canonicalBytes = encoder.encode((0, import_common3.canonicalize)(signedUpdate));
|
|
291
394
|
const nonce = (0, import_utils.randomBytes)(32);
|
|
292
395
|
entries.push({ did, nonce, signedUpdate: canonicalBytes });
|
|
293
396
|
}
|
|
@@ -329,28 +432,17 @@ var AggregationCohort = class {
|
|
|
329
432
|
}
|
|
330
433
|
};
|
|
331
434
|
|
|
332
|
-
// src/core/aggregation/messages/constants.ts
|
|
333
|
-
var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
|
|
334
|
-
var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
|
|
335
|
-
var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
336
|
-
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
337
|
-
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
338
|
-
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
339
|
-
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
340
|
-
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
341
|
-
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
342
|
-
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
343
|
-
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
344
|
-
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
345
|
-
|
|
346
435
|
// src/core/aggregation/messages/base.ts
|
|
436
|
+
var AGGREGATION_WIRE_VERSION = 1;
|
|
347
437
|
var BaseMessage = class {
|
|
348
438
|
type;
|
|
439
|
+
version;
|
|
349
440
|
to;
|
|
350
441
|
from;
|
|
351
442
|
body;
|
|
352
|
-
constructor({ type, to, from: from2, body }) {
|
|
443
|
+
constructor({ type, version, to, from: from2, body }) {
|
|
353
444
|
this.type = type;
|
|
445
|
+
this.version = version ?? AGGREGATION_WIRE_VERSION;
|
|
354
446
|
this.to = to;
|
|
355
447
|
this.from = from2;
|
|
356
448
|
this.body = body;
|
|
@@ -362,6 +454,7 @@ var BaseMessage = class {
|
|
|
362
454
|
toJSON() {
|
|
363
455
|
return {
|
|
364
456
|
type: this.type,
|
|
457
|
+
version: this.version,
|
|
365
458
|
to: this.to,
|
|
366
459
|
from: this.from,
|
|
367
460
|
body: this.body
|
|
@@ -369,6 +462,20 @@ var BaseMessage = class {
|
|
|
369
462
|
}
|
|
370
463
|
};
|
|
371
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
|
+
|
|
372
479
|
// src/core/aggregation/messages/factories.ts
|
|
373
480
|
function createCohortAdvertMessage(fields) {
|
|
374
481
|
const { from: from2, ...body } = fields;
|
|
@@ -456,8 +563,8 @@ var SigningSessionPhase = /* @__PURE__ */ ((SigningSessionPhase2) => {
|
|
|
456
563
|
})(SigningSessionPhase || {});
|
|
457
564
|
|
|
458
565
|
// src/core/aggregation/signing-session.ts
|
|
566
|
+
var import_btc_signer2 = require("@scure/btc-signer");
|
|
459
567
|
var musig2 = __toESM(require("@scure/btc-signer/musig2"), 1);
|
|
460
|
-
var import_bitcoinjs_lib2 = require("bitcoinjs-lib");
|
|
461
568
|
var BeaconSigningSession = class {
|
|
462
569
|
/** Unique identifier for this signing session. */
|
|
463
570
|
id;
|
|
@@ -499,11 +606,11 @@ var BeaconSigningSession = class {
|
|
|
499
606
|
"SIGHASH_ERROR"
|
|
500
607
|
);
|
|
501
608
|
}
|
|
502
|
-
return this.pendingTx.
|
|
609
|
+
return this.pendingTx.preimageWitnessV1(
|
|
503
610
|
0,
|
|
504
611
|
this.prevOutScripts,
|
|
505
|
-
|
|
506
|
-
|
|
612
|
+
import_btc_signer2.SigHash.DEFAULT,
|
|
613
|
+
this.prevOutValues
|
|
507
614
|
);
|
|
508
615
|
}
|
|
509
616
|
addNonceContribution(participantDid, nonceContribution) {
|
|
@@ -514,6 +621,13 @@ var BeaconSigningSession = class {
|
|
|
514
621
|
{ phase: this.phase }
|
|
515
622
|
);
|
|
516
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
|
+
}
|
|
517
631
|
if (nonceContribution.length !== 66) {
|
|
518
632
|
throw new SigningSessionError(
|
|
519
633
|
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
@@ -549,6 +663,13 @@ var BeaconSigningSession = class {
|
|
|
549
663
|
"INVALID_PHASE"
|
|
550
664
|
);
|
|
551
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
|
+
}
|
|
552
673
|
if (this.partialSignatures.has(participantDid)) {
|
|
553
674
|
throw new SigningSessionError(
|
|
554
675
|
`Duplicate partial signature from ${participantDid}.`,
|
|
@@ -574,9 +695,39 @@ var BeaconSigningSession = class {
|
|
|
574
695
|
this.aggregatedNonce,
|
|
575
696
|
this.cohort.cohortKeys,
|
|
576
697
|
this.sigHash,
|
|
577
|
-
[this.cohort.
|
|
698
|
+
[this.cohort.tapTweak],
|
|
578
699
|
[true]
|
|
579
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
|
+
}
|
|
580
731
|
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
581
732
|
this.phase = "Complete" /* Complete */;
|
|
582
733
|
return this.signature;
|
|
@@ -594,6 +745,10 @@ var BeaconSigningSession = class {
|
|
|
594
745
|
/**
|
|
595
746
|
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
596
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.
|
|
597
752
|
*/
|
|
598
753
|
generatePartialSignature(participantSecretKey) {
|
|
599
754
|
if (!this.aggregatedNonce) {
|
|
@@ -606,10 +761,13 @@ var BeaconSigningSession = class {
|
|
|
606
761
|
this.aggregatedNonce,
|
|
607
762
|
this.cohort.cohortKeys,
|
|
608
763
|
this.sigHash,
|
|
609
|
-
[this.cohort.
|
|
764
|
+
[this.cohort.tapTweak],
|
|
610
765
|
[true]
|
|
611
766
|
);
|
|
612
|
-
|
|
767
|
+
const partialSig = session.sign(this.secretNonce, participantSecretKey);
|
|
768
|
+
this.secretNonce.fill(0);
|
|
769
|
+
this.secretNonce = void 0;
|
|
770
|
+
return partialSig;
|
|
613
771
|
}
|
|
614
772
|
isComplete() {
|
|
615
773
|
return this.phase === "Complete" /* Complete */;
|
|
@@ -620,16 +778,32 @@ var BeaconSigningSession = class {
|
|
|
620
778
|
};
|
|
621
779
|
|
|
622
780
|
// src/core/aggregation/service.ts
|
|
781
|
+
var DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
|
|
623
782
|
var AggregationService = class {
|
|
624
783
|
did;
|
|
625
784
|
keys;
|
|
785
|
+
maxUpdateSizeBytes;
|
|
626
786
|
/** Per-cohort state, keyed by cohortId. */
|
|
627
787
|
#cohortStates = /* @__PURE__ */ new Map();
|
|
628
|
-
constructor({ did, keys }) {
|
|
788
|
+
constructor({ did, keys, maxUpdateSizeBytes }) {
|
|
629
789
|
this.did = did;
|
|
630
790
|
this.keys = keys;
|
|
791
|
+
this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
|
|
631
792
|
}
|
|
632
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
|
+
}
|
|
633
807
|
const type = message.type;
|
|
634
808
|
switch (type) {
|
|
635
809
|
case COHORT_OPT_IN:
|
|
@@ -651,6 +825,18 @@ var AggregationService = class {
|
|
|
651
825
|
break;
|
|
652
826
|
}
|
|
653
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
|
+
}
|
|
654
840
|
/**
|
|
655
841
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
656
842
|
* Cohort starts in `Created` phase — call `advertise()` to broadcast.
|
|
@@ -667,7 +853,8 @@ var AggregationService = class {
|
|
|
667
853
|
cohort,
|
|
668
854
|
config,
|
|
669
855
|
pendingOptIns: /* @__PURE__ */ new Map(),
|
|
670
|
-
acceptedParticipants: /* @__PURE__ */ new Set()
|
|
856
|
+
acceptedParticipants: /* @__PURE__ */ new Set(),
|
|
857
|
+
rejections: []
|
|
671
858
|
});
|
|
672
859
|
return cohort.id;
|
|
673
860
|
}
|
|
@@ -720,6 +907,7 @@ var AggregationService = class {
|
|
|
720
907
|
const participantPk = message.body?.participantPk;
|
|
721
908
|
const communicationPk = message.body?.communicationPk;
|
|
722
909
|
if (!participantPk || !communicationPk) return;
|
|
910
|
+
if (state.acceptedParticipants.has(participantDid)) return;
|
|
723
911
|
state.pendingOptIns.set(participantDid, {
|
|
724
912
|
cohortId,
|
|
725
913
|
participantDid,
|
|
@@ -753,6 +941,7 @@ var AggregationService = class {
|
|
|
753
941
|
}
|
|
754
942
|
state.acceptedParticipants.add(participantDid);
|
|
755
943
|
state.cohort.participants.push(participantDid);
|
|
944
|
+
state.cohort.participantKeys.set(participantDid, optIn.participantPk);
|
|
756
945
|
state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
|
|
757
946
|
return [createCohortOptInAcceptMessage({
|
|
758
947
|
from: this.did,
|
|
@@ -803,6 +992,13 @@ var AggregationService = class {
|
|
|
803
992
|
if (!state) return /* @__PURE__ */ new Map();
|
|
804
993
|
return state.cohort.pendingUpdates;
|
|
805
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
|
+
*/
|
|
806
1002
|
#handleSubmitUpdate(message) {
|
|
807
1003
|
const cohortId = message.body?.cohortId;
|
|
808
1004
|
if (!cohortId) return;
|
|
@@ -810,7 +1006,31 @@ var AggregationService = class {
|
|
|
810
1006
|
if (!state) return;
|
|
811
1007
|
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
812
1008
|
const signedUpdate = message.body?.signedUpdate;
|
|
813
|
-
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
|
+
}
|
|
814
1034
|
state.cohort.addUpdate(message.from, signedUpdate);
|
|
815
1035
|
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
816
1036
|
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
@@ -819,6 +1039,36 @@ var AggregationService = class {
|
|
|
819
1039
|
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
820
1040
|
}
|
|
821
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
|
+
}
|
|
822
1072
|
/**
|
|
823
1073
|
* Build the aggregated data structure (CAS Announcement or SMT tree) and
|
|
824
1074
|
* return distribute messages to send to all participants for validation.
|
|
@@ -835,29 +1085,28 @@ var AggregationService = class {
|
|
|
835
1085
|
{ cohortId, phase: state.phase }
|
|
836
1086
|
);
|
|
837
1087
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
} else if (state.config.beaconType === "SMTBeacon") {
|
|
841
|
-
state.cohort.buildSMTTree();
|
|
842
|
-
} else {
|
|
1088
|
+
const strategy = getBeaconStrategy(state.config.beaconType);
|
|
1089
|
+
if (!strategy) {
|
|
843
1090
|
throw new AggregationServiceError(
|
|
844
1091
|
`Unsupported beacon type: ${state.config.beaconType}`,
|
|
845
1092
|
"UNSUPPORTED_BEACON_TYPE",
|
|
846
1093
|
{ cohortId, beaconType: state.config.beaconType }
|
|
847
1094
|
);
|
|
848
1095
|
}
|
|
1096
|
+
strategy.buildAggregatedData(state.cohort);
|
|
849
1097
|
const signalBytesHex = (0, import_utils2.bytesToHex)(state.cohort.signalBytes);
|
|
850
1098
|
state.phase = "DataDistributed" /* DataDistributed */;
|
|
851
1099
|
const messages = [];
|
|
852
1100
|
for (const participantDid of state.cohort.participants) {
|
|
1101
|
+
const payload = strategy.getDistributePayload(state.cohort, participantDid);
|
|
853
1102
|
messages.push(createDistributeAggregatedDataMessage({
|
|
854
1103
|
from: this.did,
|
|
855
1104
|
to: participantDid,
|
|
856
1105
|
cohortId,
|
|
857
1106
|
beaconType: state.config.beaconType,
|
|
858
1107
|
signalBytesHex,
|
|
859
|
-
casAnnouncement:
|
|
860
|
-
smtProof:
|
|
1108
|
+
casAnnouncement: payload.casAnnouncement,
|
|
1109
|
+
smtProof: payload.smtProof
|
|
861
1110
|
}));
|
|
862
1111
|
}
|
|
863
1112
|
return messages;
|
|
@@ -919,6 +1168,14 @@ var AggregationService = class {
|
|
|
919
1168
|
});
|
|
920
1169
|
state.signingSession = session;
|
|
921
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
|
+
}
|
|
922
1179
|
const messages = [];
|
|
923
1180
|
for (const participantDid of state.cohort.participants) {
|
|
924
1181
|
messages.push(createAuthorizationRequestMessage({
|
|
@@ -926,7 +1183,8 @@ var AggregationService = class {
|
|
|
926
1183
|
to: participantDid,
|
|
927
1184
|
cohortId,
|
|
928
1185
|
sessionId: session.id,
|
|
929
|
-
pendingTx: txData.tx.
|
|
1186
|
+
pendingTx: txData.tx.hex,
|
|
1187
|
+
prevOutScriptHex: (0, import_utils2.bytesToHex)(prevOutScript),
|
|
930
1188
|
prevOutValue: txData.prevOutValues[0]?.toString() ?? "0"
|
|
931
1189
|
}));
|
|
932
1190
|
}
|
|
@@ -990,7 +1248,7 @@ var AggregationService = class {
|
|
|
990
1248
|
state.signingSession.addPartialSignature(message.from, partialSignature);
|
|
991
1249
|
if (state.signingSession.partialSignatures.size === state.cohort.participants.length) {
|
|
992
1250
|
const signature = state.signingSession.generateFinalSignature();
|
|
993
|
-
state.signingSession.pendingTx.
|
|
1251
|
+
state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
|
|
994
1252
|
state.result = {
|
|
995
1253
|
cohortId,
|
|
996
1254
|
signature,
|
|
@@ -1019,13 +1277,19 @@ var AggregationService = class {
|
|
|
1019
1277
|
get cohorts() {
|
|
1020
1278
|
return [...this.#cohortStates.values()].map((s) => s.cohort);
|
|
1021
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
|
+
}
|
|
1022
1287
|
};
|
|
1023
1288
|
|
|
1024
1289
|
// src/core/aggregation/participant.ts
|
|
1025
|
-
var
|
|
1026
|
-
var import_smt2 = require("@did-btcr2/smt");
|
|
1290
|
+
var import_common5 = require("@did-btcr2/common");
|
|
1027
1291
|
var import_utils3 = require("@noble/hashes/utils");
|
|
1028
|
-
var
|
|
1292
|
+
var import_btc_signer3 = require("@scure/btc-signer");
|
|
1029
1293
|
var AggregationParticipant = class {
|
|
1030
1294
|
did;
|
|
1031
1295
|
keys;
|
|
@@ -1040,6 +1304,9 @@ var AggregationParticipant = class {
|
|
|
1040
1304
|
* outgoing messages — those come exclusively from action methods.
|
|
1041
1305
|
*/
|
|
1042
1306
|
receive(message) {
|
|
1307
|
+
if (message.version === void 0 || message.version !== AGGREGATION_WIRE_VERSION) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1043
1310
|
const type = message.type;
|
|
1044
1311
|
switch (type) {
|
|
1045
1312
|
case COHORT_ADVERT:
|
|
@@ -1196,42 +1463,26 @@ var AggregationParticipant = class {
|
|
|
1196
1463
|
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
|
|
1197
1464
|
if (!state.submittedUpdate) return;
|
|
1198
1465
|
const beaconType = message.body?.beaconType;
|
|
1466
|
+
if (!beaconType) return;
|
|
1467
|
+
const strategy = getBeaconStrategy(beaconType);
|
|
1468
|
+
if (!strategy) return;
|
|
1199
1469
|
const signalBytesHex = message.body?.signalBytesHex ?? "";
|
|
1200
|
-
const expectedHash = (0,
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
const smtProof = message.body?.smtProof;
|
|
1217
|
-
if (smtProof?.updateId && smtProof?.nonce) {
|
|
1218
|
-
const canonicalBytes = new TextEncoder().encode((0, import_common3.canonicalize)(state.submittedUpdate));
|
|
1219
|
-
const expectedUpdateId = (0, import_smt2.hashToHex)((0, import_smt2.blockHash)(canonicalBytes));
|
|
1220
|
-
if (smtProof.updateId === expectedUpdateId) {
|
|
1221
|
-
const index = (0, import_smt2.didToIndex)(this.did);
|
|
1222
|
-
const candidateHash = (0, import_smt2.blockHash)((0, import_smt2.blockHash)((0, import_smt2.hexToHash)(smtProof.nonce)), (0, import_smt2.hexToHash)(smtProof.updateId));
|
|
1223
|
-
matches = (0, import_smt2.verifySerializedProof)(smtProof, index, candidateHash);
|
|
1224
|
-
}
|
|
1225
|
-
state.validation = {
|
|
1226
|
-
cohortId,
|
|
1227
|
-
beaconType,
|
|
1228
|
-
signalBytesHex,
|
|
1229
|
-
smtProof,
|
|
1230
|
-
expectedHash,
|
|
1231
|
-
matches
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
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
|
+
};
|
|
1235
1486
|
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
1236
1487
|
}
|
|
1237
1488
|
/**
|
|
@@ -1292,12 +1543,14 @@ var AggregationParticipant = class {
|
|
|
1292
1543
|
if (state.phase !== "ValidationSent" /* ValidationSent */) return;
|
|
1293
1544
|
const sessionId = message.body?.sessionId;
|
|
1294
1545
|
const pendingTxHex = message.body?.pendingTx;
|
|
1546
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
1295
1547
|
const prevOutValue = message.body?.prevOutValue;
|
|
1296
|
-
if (!sessionId || !pendingTxHex || !prevOutValue) return;
|
|
1548
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
|
|
1297
1549
|
state.signingRequest = {
|
|
1298
1550
|
cohortId,
|
|
1299
1551
|
sessionId,
|
|
1300
1552
|
pendingTxHex,
|
|
1553
|
+
prevOutScriptHex,
|
|
1301
1554
|
prevOutValue
|
|
1302
1555
|
};
|
|
1303
1556
|
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
@@ -1321,8 +1574,8 @@ var AggregationParticipant = class {
|
|
|
1321
1574
|
{ cohortId }
|
|
1322
1575
|
);
|
|
1323
1576
|
}
|
|
1324
|
-
const tx =
|
|
1325
|
-
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)];
|
|
1326
1579
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
1327
1580
|
const session = new BeaconSigningSession({
|
|
1328
1581
|
id: state.signingRequest.sessionId,
|
|
@@ -1392,6 +1645,67 @@ var AggregationParticipant = class {
|
|
|
1392
1645
|
}
|
|
1393
1646
|
};
|
|
1394
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
|
+
|
|
1395
1709
|
// src/core/aggregation/messages/guards.ts
|
|
1396
1710
|
var KEYGEN_VALUES = /* @__PURE__ */ new Set([
|
|
1397
1711
|
COHORT_ADVERT,
|
|
@@ -1424,20 +1738,20 @@ function isSignMessageType(type) {
|
|
|
1424
1738
|
}
|
|
1425
1739
|
|
|
1426
1740
|
// src/core/aggregation/transport/error.ts
|
|
1427
|
-
var
|
|
1428
|
-
var TransportError = class extends
|
|
1741
|
+
var import_common6 = require("@did-btcr2/common");
|
|
1742
|
+
var TransportError = class extends import_common6.MethodError {
|
|
1429
1743
|
constructor(message, type = "TransportError", data) {
|
|
1430
1744
|
super(message, type, data);
|
|
1431
1745
|
}
|
|
1432
1746
|
};
|
|
1433
|
-
var TransportAdapterError = class extends
|
|
1747
|
+
var TransportAdapterError = class extends import_common6.MethodError {
|
|
1434
1748
|
constructor(message, type = "TransportAdapterError", data) {
|
|
1435
1749
|
super(message, type, data);
|
|
1436
1750
|
}
|
|
1437
1751
|
};
|
|
1438
1752
|
|
|
1439
1753
|
// src/core/aggregation/transport/factory.ts
|
|
1440
|
-
var
|
|
1754
|
+
var import_common7 = require("@did-btcr2/common");
|
|
1441
1755
|
|
|
1442
1756
|
// src/core/aggregation/transport/nostr.ts
|
|
1443
1757
|
var import_keypair = require("@did-btcr2/keypair");
|
|
@@ -1450,6 +1764,7 @@ var DEFAULT_NOSTR_RELAYS = [
|
|
|
1450
1764
|
"wss://relay.snort.social",
|
|
1451
1765
|
"wss://nostr-pub.wellorder.net"
|
|
1452
1766
|
];
|
|
1767
|
+
var DEFAULT_BROADCAST_LOOKBACK_MS = 5 * 60 * 1e3;
|
|
1453
1768
|
var NostrTransport = class _NostrTransport {
|
|
1454
1769
|
name = "nostr";
|
|
1455
1770
|
pool;
|
|
@@ -1457,8 +1772,12 @@ var NostrTransport = class _NostrTransport {
|
|
|
1457
1772
|
#actors = /* @__PURE__ */ new Map();
|
|
1458
1773
|
#peerRegistry = /* @__PURE__ */ new Map();
|
|
1459
1774
|
#started = false;
|
|
1775
|
+
#logger;
|
|
1776
|
+
#broadcastLookbackMs;
|
|
1460
1777
|
constructor(config) {
|
|
1461
1778
|
this.#relays = config?.relays ?? DEFAULT_NOSTR_RELAYS;
|
|
1779
|
+
this.#logger = config?.logger ?? CONSOLE_LOGGER;
|
|
1780
|
+
this.#broadcastLookbackMs = config?.broadcastLookbackMs ?? DEFAULT_BROADCAST_LOOKBACK_MS;
|
|
1462
1781
|
}
|
|
1463
1782
|
/**
|
|
1464
1783
|
* Registers an actor (DID + keys) to send/receive messages with.
|
|
@@ -1469,19 +1788,68 @@ var NostrTransport = class _NostrTransport {
|
|
|
1469
1788
|
* @example
|
|
1470
1789
|
* const transport = new NostrTransport();
|
|
1471
1790
|
* const keys = SchnorrKeyPair.generate();
|
|
1472
|
-
*
|
|
1791
|
+
* const did = DidBtcr2.create(keys.publicKey.compressed, { idType: 'KEY', network: 'mutinynet' });
|
|
1792
|
+
* transport.registerActor(did, keys);
|
|
1473
1793
|
* transport.start();
|
|
1474
1794
|
*/
|
|
1475
1795
|
registerActor(did, keys) {
|
|
1476
|
-
const entry = { keys, handlers: /* @__PURE__ */ new Map() };
|
|
1796
|
+
const entry = { keys, handlers: /* @__PURE__ */ new Map(), subscriptions: [] };
|
|
1477
1797
|
this.#actors.set(did, entry);
|
|
1478
1798
|
if (this.#started && this.pool) {
|
|
1479
1799
|
this.#subscribeDirected(did, entry);
|
|
1480
1800
|
}
|
|
1481
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
|
+
*/
|
|
1482
1845
|
getActorPk(did) {
|
|
1483
1846
|
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
1484
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
|
+
*/
|
|
1485
1853
|
registerPeer(did, communicationPk) {
|
|
1486
1854
|
try {
|
|
1487
1855
|
new import_keypair.CompressedSecp256k1PublicKey(communicationPk);
|
|
@@ -1494,9 +1862,25 @@ var NostrTransport = class _NostrTransport {
|
|
|
1494
1862
|
}
|
|
1495
1863
|
this.#peerRegistry.set(did, communicationPk);
|
|
1496
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
|
+
*/
|
|
1497
1871
|
getPeerPk(did) {
|
|
1498
1872
|
return this.#peerRegistry.get(did);
|
|
1499
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
|
+
*/
|
|
1500
1884
|
registerMessageHandler(actorDid, messageType, handler) {
|
|
1501
1885
|
const actor = this.#actors.get(actorDid);
|
|
1502
1886
|
if (!actor) {
|
|
@@ -1508,21 +1892,48 @@ var NostrTransport = class _NostrTransport {
|
|
|
1508
1892
|
}
|
|
1509
1893
|
actor.handlers.set(messageType, handler);
|
|
1510
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
|
+
*/
|
|
1511
1904
|
start() {
|
|
1512
1905
|
if (this.#started) return this;
|
|
1513
1906
|
this.#started = true;
|
|
1514
1907
|
this.pool = new import_pool.SimplePool();
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
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),
|
|
1518
1917
|
onevent: this.#handleBroadcastEvent.bind(this)
|
|
1519
1918
|
});
|
|
1520
1919
|
for (const [did, entry] of this.#actors) {
|
|
1521
1920
|
this.#subscribeDirected(did, entry);
|
|
1522
1921
|
}
|
|
1523
|
-
|
|
1922
|
+
this.#logger.info(`NostrTransport started, listening on ${this.#relays.length} relay(s)`);
|
|
1524
1923
|
return this;
|
|
1525
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
|
+
*/
|
|
1526
1937
|
async sendMessage(message, sender, to) {
|
|
1527
1938
|
const type = message.type;
|
|
1528
1939
|
if (!type) {
|
|
@@ -1559,7 +1970,7 @@ var NostrTransport = class _NostrTransport {
|
|
|
1559
1970
|
tags,
|
|
1560
1971
|
content: JSON.stringify(message, _NostrTransport.#jsonReplacer)
|
|
1561
1972
|
}, senderKeys.secretKey.bytes);
|
|
1562
|
-
|
|
1973
|
+
this.#logger.debug(`Publishing kind 1 [${type}]`);
|
|
1563
1974
|
await this.#publishToRelays(event);
|
|
1564
1975
|
return;
|
|
1565
1976
|
}
|
|
@@ -1591,25 +2002,70 @@ var NostrTransport = class _NostrTransport {
|
|
|
1591
2002
|
tags,
|
|
1592
2003
|
content
|
|
1593
2004
|
}, senderKeys.secretKey.bytes);
|
|
1594
|
-
|
|
2005
|
+
this.#logger.debug(`Publishing kind 1059 [${type}]`);
|
|
1595
2006
|
await this.#publishToRelays(event);
|
|
1596
2007
|
return;
|
|
1597
2008
|
}
|
|
1598
|
-
|
|
2009
|
+
this.#logger.warn(`Unsupported message type: ${type}`);
|
|
1599
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
|
+
*/
|
|
1600
2048
|
#subscribeDirected(did, entry) {
|
|
1601
2049
|
if (!this.pool) return;
|
|
1602
2050
|
const pkHex = (0, import_utils4.bytesToHex)(entry.keys.publicKey.x);
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
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),
|
|
1606
2053
|
onevent: this.#makeActorEventHandler(did)
|
|
1607
2054
|
});
|
|
2055
|
+
entry.subscriptions.push(sub);
|
|
1608
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
|
+
*/
|
|
1609
2064
|
#makeActorEventHandler(actorDid) {
|
|
1610
2065
|
return async (event) => {
|
|
1611
2066
|
const actor = this.#actors.get(actorDid);
|
|
1612
2067
|
if (!actor) return;
|
|
2068
|
+
if (event.pubkey === (0, import_utils4.bytesToHex)(actor.keys.publicKey.x)) return;
|
|
1613
2069
|
let message;
|
|
1614
2070
|
try {
|
|
1615
2071
|
if (event.kind === 1) {
|
|
@@ -1625,19 +2081,29 @@ var NostrTransport = class _NostrTransport {
|
|
|
1625
2081
|
return;
|
|
1626
2082
|
}
|
|
1627
2083
|
} catch (err) {
|
|
1628
|
-
|
|
2084
|
+
this.#logger.debug(`Failed to parse event ${event.id} for ${actorDid}:`, err);
|
|
1629
2085
|
return;
|
|
1630
2086
|
}
|
|
1631
2087
|
this.#dispatchMessage(message, actor);
|
|
1632
2088
|
};
|
|
1633
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
|
+
*/
|
|
1634
2100
|
async #handleBroadcastEvent(event) {
|
|
1635
2101
|
if (event.kind !== 1) return;
|
|
1636
2102
|
let message;
|
|
1637
2103
|
try {
|
|
1638
2104
|
message = JSON.parse(event.content, _NostrTransport.#jsonReviver);
|
|
1639
2105
|
} catch (err) {
|
|
1640
|
-
|
|
2106
|
+
this.#logger.debug(`Failed to parse broadcast event ${event.id}:`, err);
|
|
1641
2107
|
return;
|
|
1642
2108
|
}
|
|
1643
2109
|
if (message.body && typeof message.body === "object") {
|
|
@@ -1650,6 +2116,18 @@ var NostrTransport = class _NostrTransport {
|
|
|
1650
2116
|
if (handler) await handler(message);
|
|
1651
2117
|
}
|
|
1652
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
|
+
*/
|
|
1653
2131
|
#dispatchMessage(message, actor) {
|
|
1654
2132
|
if (message.body && typeof message.body === "object") {
|
|
1655
2133
|
message = { ...message, ...message.body };
|
|
@@ -1659,6 +2137,16 @@ var NostrTransport = class _NostrTransport {
|
|
|
1659
2137
|
const handler = actor.handlers.get(messageType);
|
|
1660
2138
|
if (handler) handler(message);
|
|
1661
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
|
+
*/
|
|
1662
2150
|
async #publishToRelays(event) {
|
|
1663
2151
|
const relayPromises = this.pool?.publish(this.#relays, event);
|
|
1664
2152
|
if (!relayPromises?.length) return;
|
|
@@ -1666,7 +2154,7 @@ var NostrTransport = class _NostrTransport {
|
|
|
1666
2154
|
const accepted = results.filter((r) => r.status === "fulfilled").length;
|
|
1667
2155
|
const rejected = results.filter((r) => r.status === "rejected");
|
|
1668
2156
|
for (const r of rejected) {
|
|
1669
|
-
|
|
2157
|
+
this.#logger.debug(`Relay rejected event ${event.id}: ${r.reason}`);
|
|
1670
2158
|
}
|
|
1671
2159
|
if (accepted === 0) {
|
|
1672
2160
|
throw new TransportAdapterError(
|
|
@@ -1676,12 +2164,36 @@ var NostrTransport = class _NostrTransport {
|
|
|
1676
2164
|
);
|
|
1677
2165
|
}
|
|
1678
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
|
+
*/
|
|
1679
2179
|
static #jsonReplacer(_key, value) {
|
|
1680
2180
|
if (value instanceof Uint8Array) {
|
|
1681
2181
|
return { __bytes: (0, import_utils4.bytesToHex)(value) };
|
|
1682
2182
|
}
|
|
1683
2183
|
return value;
|
|
1684
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
|
+
*/
|
|
1685
2197
|
static #jsonReviver(_key, value) {
|
|
1686
2198
|
if (value && typeof value === "object" && "__bytes" in value) {
|
|
1687
2199
|
return (0, import_utils4.hexToBytes)(value.__bytes);
|
|
@@ -1697,7 +2209,7 @@ var TransportFactory = class {
|
|
|
1697
2209
|
case "nostr":
|
|
1698
2210
|
return new NostrTransport({ relays: config.relays });
|
|
1699
2211
|
case "didcomm":
|
|
1700
|
-
throw new
|
|
2212
|
+
throw new import_common7.NotImplementedError("DIDComm transport not implemented yet.");
|
|
1701
2213
|
default:
|
|
1702
2214
|
throw new TransportError(
|
|
1703
2215
|
`Invalid transport type: ${config.type}`,
|
|
@@ -1709,29 +2221,38 @@ var TransportFactory = class {
|
|
|
1709
2221
|
};
|
|
1710
2222
|
|
|
1711
2223
|
// src/core/aggregation/transport/didcomm.ts
|
|
1712
|
-
var
|
|
2224
|
+
var import_common8 = require("@did-btcr2/common");
|
|
1713
2225
|
var DidCommTransport = class {
|
|
1714
2226
|
name = "didcomm";
|
|
1715
2227
|
start() {
|
|
1716
|
-
throw new
|
|
2228
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented. Use NostrTransport instead.");
|
|
1717
2229
|
}
|
|
1718
2230
|
registerActor(_did, _keys) {
|
|
1719
|
-
throw new
|
|
2231
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1720
2232
|
}
|
|
1721
2233
|
getActorPk(_did) {
|
|
1722
|
-
throw new
|
|
2234
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1723
2235
|
}
|
|
1724
2236
|
registerPeer(_did, _communicationPk) {
|
|
1725
|
-
throw new
|
|
2237
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1726
2238
|
}
|
|
1727
2239
|
getPeerPk(_did) {
|
|
1728
|
-
throw new
|
|
2240
|
+
throw new import_common8.NotImplementedError("DidCommTransport not implemented.");
|
|
1729
2241
|
}
|
|
1730
2242
|
registerMessageHandler(_actorDid, _messageType, _handler) {
|
|
1731
|
-
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.");
|
|
1732
2250
|
}
|
|
1733
2251
|
async sendMessage(_message, _sender, _recipient) {
|
|
1734
|
-
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.");
|
|
1735
2256
|
}
|
|
1736
2257
|
};
|
|
1737
2258
|
|
|
@@ -1790,7 +2311,8 @@ var TypedEventEmitter = class {
|
|
|
1790
2311
|
};
|
|
1791
2312
|
|
|
1792
2313
|
// src/core/aggregation/runner/service-runner.ts
|
|
1793
|
-
var
|
|
2314
|
+
var DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 6e4;
|
|
2315
|
+
var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEventEmitter {
|
|
1794
2316
|
/** Direct access to the underlying state machine for advanced use. */
|
|
1795
2317
|
session;
|
|
1796
2318
|
#transport;
|
|
@@ -1799,11 +2321,26 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1799
2321
|
#onOptInReceived;
|
|
1800
2322
|
#onReadyToFinalize;
|
|
1801
2323
|
#onProvideTxData;
|
|
2324
|
+
#cohortTtlMs;
|
|
2325
|
+
#phaseTimeoutMs;
|
|
2326
|
+
#advertRepeatIntervalMs;
|
|
1802
2327
|
#cohortId;
|
|
1803
2328
|
#handlersRegistered = false;
|
|
1804
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;
|
|
1805
2337
|
#resolveRun;
|
|
1806
2338
|
#rejectRun;
|
|
2339
|
+
#cohortTtlTimer;
|
|
2340
|
+
#phaseTimer;
|
|
2341
|
+
#lastObservedPhase;
|
|
2342
|
+
/** Stop handle for the repeating COHORT_ADVERT publish loop. */
|
|
2343
|
+
#stopAdvertRepeat;
|
|
1807
2344
|
constructor(options) {
|
|
1808
2345
|
super();
|
|
1809
2346
|
this.#transport = options.transport;
|
|
@@ -1814,7 +2351,25 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1814
2351
|
finalize: acceptedCount >= minRequired
|
|
1815
2352
|
}));
|
|
1816
2353
|
this.#onProvideTxData = options.onProvideTxData;
|
|
1817
|
-
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
|
+
}
|
|
1818
2373
|
}
|
|
1819
2374
|
/**
|
|
1820
2375
|
* Run the protocol to completion. Resolves with the final aggregation result
|
|
@@ -1829,22 +2384,103 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1829
2384
|
try {
|
|
1830
2385
|
this.#registerHandlers();
|
|
1831
2386
|
this.#cohortId = this.session.createCohort(this.#config);
|
|
2387
|
+
this.#startTimers();
|
|
1832
2388
|
const advertMsgs = this.session.advertise(this.#cohortId);
|
|
2389
|
+
this.#onPhaseMaybeChanged();
|
|
1833
2390
|
this.emit("cohort-advertised", { cohortId: this.#cohortId });
|
|
1834
|
-
this.#
|
|
2391
|
+
if (this.#advertRepeatIntervalMs > 0) {
|
|
2392
|
+
this.#startAdvertRepeat(advertMsgs);
|
|
2393
|
+
} else {
|
|
2394
|
+
this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
|
|
2395
|
+
}
|
|
1835
2396
|
} catch (err) {
|
|
1836
2397
|
this.#fail(err);
|
|
1837
2398
|
}
|
|
1838
2399
|
});
|
|
1839
2400
|
}
|
|
1840
2401
|
/**
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
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.
|
|
1844
2468
|
*/
|
|
1845
2469
|
stop() {
|
|
1846
2470
|
this.#stopped = true;
|
|
1847
|
-
|
|
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
|
+
];
|
|
1848
2484
|
/**
|
|
1849
2485
|
* Internal: handler registration with the transport. Idempotent.
|
|
1850
2486
|
*/
|
|
@@ -1857,6 +2493,14 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1857
2493
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
1858
2494
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
1859
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
|
+
}
|
|
1860
2504
|
/**
|
|
1861
2505
|
* Internal: message handlers for each protocol step. Each handler:
|
|
1862
2506
|
* 1) feeds the message into the state machine via session.receive()
|
|
@@ -1873,6 +2517,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1873
2517
|
if (this.#stopped) return;
|
|
1874
2518
|
try {
|
|
1875
2519
|
this.session.receive(msg);
|
|
2520
|
+
this.#drainRejections();
|
|
2521
|
+
this.#onPhaseMaybeChanged();
|
|
1876
2522
|
const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
|
|
1877
2523
|
if (!optIn) return;
|
|
1878
2524
|
this.emit("opt-in-received", optIn);
|
|
@@ -1884,19 +2530,23 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1884
2530
|
await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
|
|
1885
2531
|
this.emit("participant-accepted", { participantDid: msg.from });
|
|
1886
2532
|
const cohort = this.session.getCohort(this.#cohortId);
|
|
1887
|
-
if (cohort.participants.length >= this.#config.minParticipants) {
|
|
2533
|
+
if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
|
|
2534
|
+
this.#finalizing = true;
|
|
1888
2535
|
const finalizeDecision = await this.#onReadyToFinalize({
|
|
1889
2536
|
acceptedCount: cohort.participants.length,
|
|
1890
2537
|
minRequired: this.#config.minParticipants
|
|
1891
2538
|
});
|
|
1892
|
-
if (finalizeDecision.finalize) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
cohortId: this.#cohortId,
|
|
1896
|
-
beaconAddress: cohort.beaconAddress
|
|
1897
|
-
});
|
|
1898
|
-
await this.#sendAll(readyMsgs);
|
|
2539
|
+
if (!finalizeDecision.finalize) {
|
|
2540
|
+
this.#finalizing = false;
|
|
2541
|
+
return;
|
|
1899
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);
|
|
1900
2550
|
}
|
|
1901
2551
|
} catch (err) {
|
|
1902
2552
|
this.#fail(err);
|
|
@@ -1914,6 +2564,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1914
2564
|
if (this.#stopped) return;
|
|
1915
2565
|
try {
|
|
1916
2566
|
this.session.receive(msg);
|
|
2567
|
+
this.#drainRejections();
|
|
2568
|
+
this.#onPhaseMaybeChanged();
|
|
1917
2569
|
this.emit("update-received", { participantDid: msg.from });
|
|
1918
2570
|
if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
|
|
1919
2571
|
const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
|
|
@@ -1936,9 +2588,18 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1936
2588
|
if (this.#stopped) return;
|
|
1937
2589
|
try {
|
|
1938
2590
|
this.session.receive(msg);
|
|
2591
|
+
this.#drainRejections();
|
|
2592
|
+
this.#onPhaseMaybeChanged();
|
|
1939
2593
|
const approved = !!msg.body?.approved;
|
|
1940
2594
|
this.emit("validation-received", { participantDid: msg.from, approved });
|
|
1941
|
-
|
|
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 */) {
|
|
1942
2603
|
const cohort = this.session.getCohort(this.#cohortId);
|
|
1943
2604
|
const txData = await this.#onProvideTxData({
|
|
1944
2605
|
cohortId: this.#cohortId,
|
|
@@ -1966,6 +2627,8 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1966
2627
|
if (this.#stopped) return;
|
|
1967
2628
|
try {
|
|
1968
2629
|
this.session.receive(msg);
|
|
2630
|
+
this.#drainRejections();
|
|
2631
|
+
this.#onPhaseMaybeChanged();
|
|
1969
2632
|
this.emit("nonce-received", { participantDid: msg.from });
|
|
1970
2633
|
if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
|
|
1971
2634
|
await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
|
|
@@ -1986,8 +2649,12 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
1986
2649
|
if (this.#stopped) return;
|
|
1987
2650
|
try {
|
|
1988
2651
|
this.session.receive(msg);
|
|
2652
|
+
this.#drainRejections();
|
|
2653
|
+
this.#onPhaseMaybeChanged();
|
|
1989
2654
|
const result = this.session.getResult(this.#cohortId);
|
|
1990
2655
|
if (result) {
|
|
2656
|
+
this.#clearTimers();
|
|
2657
|
+
this.#unregisterHandlers();
|
|
1991
2658
|
this.emit("signing-complete", result);
|
|
1992
2659
|
this.#resolveRun?.(result);
|
|
1993
2660
|
}
|
|
@@ -2012,6 +2679,10 @@ var AggregationServiceRunner = class extends TypedEventEmitter {
|
|
|
2012
2679
|
* @param {Error} err - The error to handle.
|
|
2013
2680
|
*/
|
|
2014
2681
|
#fail(err) {
|
|
2682
|
+
this.#stopAdvertRepeating();
|
|
2683
|
+
this.#clearTimers();
|
|
2684
|
+
this.#unregisterHandlers();
|
|
2685
|
+
if (this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
2015
2686
|
this.emit("error", err);
|
|
2016
2687
|
this.#rejectRun?.(err);
|
|
2017
2688
|
}
|
|
@@ -2046,9 +2717,27 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2046
2717
|
async start() {
|
|
2047
2718
|
this.#registerHandlers();
|
|
2048
2719
|
}
|
|
2049
|
-
/** Stop the runner
|
|
2720
|
+
/** Stop the runner and detach transport handlers. Safe to call repeatedly. */
|
|
2050
2721
|
stop() {
|
|
2051
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
|
+
}
|
|
2052
2741
|
}
|
|
2053
2742
|
/**
|
|
2054
2743
|
* Single-shot helper: start, join the first cohort that passes `shouldJoin`,
|
|
@@ -2213,7 +2902,14 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2213
2902
|
if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
|
|
2214
2903
|
const info = this.session.joinedCohorts.get(cohortId);
|
|
2215
2904
|
if (info) {
|
|
2216
|
-
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
|
+
});
|
|
2217
2913
|
}
|
|
2218
2914
|
}
|
|
2219
2915
|
} catch (err) {
|
|
@@ -2235,33 +2931,33 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
2235
2931
|
};
|
|
2236
2932
|
|
|
2237
2933
|
// src/core/beacon/beacon.ts
|
|
2238
|
-
var
|
|
2934
|
+
var import_secp256k12 = require("@noble/secp256k1");
|
|
2239
2935
|
var import_utils5 = require("@noble/hashes/utils");
|
|
2240
|
-
var
|
|
2936
|
+
var import_btc_signer4 = require("@scure/btc-signer");
|
|
2241
2937
|
|
|
2242
2938
|
// src/core/beacon/error.ts
|
|
2243
|
-
var
|
|
2244
|
-
var BeaconError = class extends
|
|
2939
|
+
var import_common9 = require("@did-btcr2/common");
|
|
2940
|
+
var BeaconError = class extends import_common9.MethodError {
|
|
2245
2941
|
constructor(message, type = "BeaconError", data) {
|
|
2246
2942
|
super(message, type, data);
|
|
2247
2943
|
}
|
|
2248
2944
|
};
|
|
2249
|
-
var SingletonBeaconError = class extends
|
|
2945
|
+
var SingletonBeaconError = class extends import_common9.MethodError {
|
|
2250
2946
|
constructor(message, type = "SingletonBeaconError", data) {
|
|
2251
2947
|
super(message, type, data);
|
|
2252
2948
|
}
|
|
2253
2949
|
};
|
|
2254
|
-
var AggregateBeaconError = class extends
|
|
2950
|
+
var AggregateBeaconError = class extends import_common9.MethodError {
|
|
2255
2951
|
constructor(message, type = "AggregateBeaconError", data) {
|
|
2256
2952
|
super(message, type, data);
|
|
2257
2953
|
}
|
|
2258
2954
|
};
|
|
2259
|
-
var CASBeaconError = class extends
|
|
2955
|
+
var CASBeaconError = class extends import_common9.MethodError {
|
|
2260
2956
|
constructor(message, type = "CASBeaconError", data) {
|
|
2261
2957
|
super(message, type, data);
|
|
2262
2958
|
}
|
|
2263
2959
|
};
|
|
2264
|
-
var SMTBeaconError = class extends
|
|
2960
|
+
var SMTBeaconError = class extends import_common9.MethodError {
|
|
2265
2961
|
constructor(message, type = "SMTBeaconError", data) {
|
|
2266
2962
|
super(message, type, data);
|
|
2267
2963
|
}
|
|
@@ -2289,6 +2985,62 @@ var StaticFeeEstimator = class {
|
|
|
2289
2985
|
|
|
2290
2986
|
// src/core/beacon/beacon.ts
|
|
2291
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
|
+
}
|
|
2292
3044
|
var Beacon = class {
|
|
2293
3045
|
/**
|
|
2294
3046
|
* The Beacon service configuration parsed from the DID Document.
|
|
@@ -2298,79 +3050,108 @@ var Beacon = class {
|
|
|
2298
3050
|
this.service = service;
|
|
2299
3051
|
}
|
|
2300
3052
|
/**
|
|
2301
|
-
*
|
|
3053
|
+
* Build + sign + broadcast a single-party beacon signal transaction (P2WPKH spend).
|
|
2302
3054
|
*
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
* 5. Build a PSBT: input (UTXO) → change output + OP_RETURN(signalBytes).
|
|
2309
|
-
* 6. Compute the fee via the supplied (or default) {@link FeeEstimator} against the tx vsize.
|
|
2310
|
-
* 7. Sign input 0 with an ECDSA signer derived from `secretKey`.
|
|
2311
|
-
* 8. Finalize, extract, and broadcast via the REST transaction endpoint.
|
|
2312
|
-
*
|
|
2313
|
-
* Fee handling: the PSBT is constructed with a placeholder change amount, signed to measure
|
|
2314
|
-
* vsize, then the change is adjusted to pay the actual fee and the input re-signed. This
|
|
2315
|
-
* two-pass approach avoids hardcoded fee constants and produces a tx that matches the
|
|
2316
|
-
* estimator's rate.
|
|
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.
|
|
2317
3060
|
*
|
|
2318
3061
|
* @param signalBytes 32-byte payload to embed in OP_RETURN.
|
|
2319
3062
|
* @param secretKey Secret key used to sign the spending input.
|
|
2320
3063
|
* @param bitcoin Bitcoin network connection.
|
|
2321
3064
|
* @param options Broadcast options (fee estimator, etc.).
|
|
2322
3065
|
* @returns The txid of the broadcast transaction.
|
|
2323
|
-
* @throws {BeaconError} if the address is unfunded
|
|
3066
|
+
* @throws {BeaconError} if the address is unfunded, no UTXO is available, or fee exceeds value.
|
|
2324
3067
|
*/
|
|
2325
3068
|
async buildSignAndBroadcast(signalBytes, secretKey, bitcoin, options) {
|
|
2326
3069
|
const feeEstimator = options?.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
|
2344
3107
|
);
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
const keyPair = import_keypair2.SchnorrKeyPair.fromSecret(secretKey);
|
|
2348
|
-
const signer = {
|
|
2349
|
-
publicKey: keyPair.publicKey.compressed,
|
|
2350
|
-
sign: (hash5) => keyPair.secretKey.sign(hash5, { scheme: "ecdsa" })
|
|
3108
|
+
tx2.addOutput({ script: opReturnScript(opts.signalBytes), amount: 0n });
|
|
3109
|
+
return tx2;
|
|
2351
3110
|
};
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
const vsize = probeTx.virtualSize();
|
|
2359
|
-
const fee = await feeEstimator.estimateFee(vsize);
|
|
2360
|
-
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) {
|
|
2361
3117
|
throw new BeaconError(
|
|
2362
|
-
`UTXO value (${utxo.value}) insufficient to cover fee (${
|
|
3118
|
+
`UTXO value (${opts.utxo.value}) insufficient to cover fee (${feeSats}).`,
|
|
2363
3119
|
"INSUFFICIENT_FUNDS",
|
|
2364
|
-
{ bitcoinAddress, utxoValue: utxo.value, fee:
|
|
3120
|
+
{ bitcoinAddress: opts.beaconAddress, utxoValue: opts.utxo.value, fee: feeSats.toString() }
|
|
2365
3121
|
);
|
|
2366
3122
|
}
|
|
2367
|
-
const
|
|
2368
|
-
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);
|
|
2369
3150
|
}
|
|
2370
3151
|
};
|
|
2371
3152
|
|
|
2372
3153
|
// src/core/beacon/cas-beacon.ts
|
|
2373
|
-
var
|
|
3154
|
+
var import_common10 = require("@did-btcr2/common");
|
|
2374
3155
|
var CASBeacon = class extends Beacon {
|
|
2375
3156
|
/**
|
|
2376
3157
|
* Creates an instance of CASBeacon.
|
|
@@ -2411,7 +3192,7 @@ var CASBeacon = class extends Beacon {
|
|
|
2411
3192
|
if (!updateHashEncoded) {
|
|
2412
3193
|
continue;
|
|
2413
3194
|
}
|
|
2414
|
-
const updateHash = (0,
|
|
3195
|
+
const updateHash = (0, import_common10.encode)((0, import_common10.decode)(updateHashEncoded, "base64urlnopad"), "hex");
|
|
2415
3196
|
const signedUpdate = sidecar.updateMap.get(updateHash);
|
|
2416
3197
|
if (!signedUpdate) {
|
|
2417
3198
|
needs.push({
|
|
@@ -2443,9 +3224,9 @@ var CASBeacon = class extends Beacon {
|
|
|
2443
3224
|
*/
|
|
2444
3225
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2445
3226
|
const did = this.service.id.split("#")[0];
|
|
2446
|
-
const updateHash = (0,
|
|
3227
|
+
const updateHash = (0, import_common10.canonicalHash)(signedUpdate);
|
|
2447
3228
|
const casAnnouncement = { [did]: updateHash };
|
|
2448
|
-
const announcementHash = (0,
|
|
3229
|
+
const announcementHash = (0, import_common10.hash)((0, import_common10.canonicalize)(casAnnouncement));
|
|
2449
3230
|
await this.buildSignAndBroadcast(announcementHash, secretKey, bitcoin, options);
|
|
2450
3231
|
if (options?.casPublish) {
|
|
2451
3232
|
await options.casPublish(casAnnouncement);
|
|
@@ -2455,10 +3236,10 @@ var CASBeacon = class extends Beacon {
|
|
|
2455
3236
|
};
|
|
2456
3237
|
|
|
2457
3238
|
// src/core/beacon/factory.ts
|
|
2458
|
-
var
|
|
3239
|
+
var import_common13 = require("@did-btcr2/common");
|
|
2459
3240
|
|
|
2460
3241
|
// src/core/beacon/singleton-beacon.ts
|
|
2461
|
-
var
|
|
3242
|
+
var import_common11 = require("@did-btcr2/common");
|
|
2462
3243
|
var SingletonBeacon = class extends Beacon {
|
|
2463
3244
|
/**
|
|
2464
3245
|
* Creates an instance of SingletonBeacon.
|
|
@@ -2506,14 +3287,14 @@ var SingletonBeacon = class extends Beacon {
|
|
|
2506
3287
|
* @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
|
|
2507
3288
|
*/
|
|
2508
3289
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2509
|
-
const signalBytes = (0,
|
|
3290
|
+
const signalBytes = (0, import_common11.hash)((0, import_common11.canonicalize)(signedUpdate));
|
|
2510
3291
|
await this.buildSignAndBroadcast(signalBytes, secretKey, bitcoin, options);
|
|
2511
3292
|
return signedUpdate;
|
|
2512
3293
|
}
|
|
2513
3294
|
};
|
|
2514
3295
|
|
|
2515
3296
|
// src/core/beacon/smt-beacon.ts
|
|
2516
|
-
var
|
|
3297
|
+
var import_common12 = require("@did-btcr2/common");
|
|
2517
3298
|
var import_smt3 = require("@did-btcr2/smt");
|
|
2518
3299
|
var import_utils6 = require("@noble/hashes/utils");
|
|
2519
3300
|
var SMTBeacon = class extends Beacon {
|
|
@@ -2601,7 +3382,7 @@ var SMTBeacon = class extends Beacon {
|
|
|
2601
3382
|
*/
|
|
2602
3383
|
async broadcastSignal(signedUpdate, secretKey, bitcoin, options) {
|
|
2603
3384
|
const did = this.service.id.split("#")[0];
|
|
2604
|
-
const canonicalBytes = new TextEncoder().encode((0,
|
|
3385
|
+
const canonicalBytes = new TextEncoder().encode((0, import_common12.canonicalize)(signedUpdate));
|
|
2605
3386
|
const nonce = (0, import_utils6.randomBytes)(32);
|
|
2606
3387
|
const tree = new import_smt3.BTCR2MerkleTree();
|
|
2607
3388
|
tree.addEntries([{ did, nonce, signedUpdate: canonicalBytes }]);
|
|
@@ -2627,19 +3408,19 @@ var BeaconFactory = class {
|
|
|
2627
3408
|
case "SMTBeacon":
|
|
2628
3409
|
return new SMTBeacon(service);
|
|
2629
3410
|
default:
|
|
2630
|
-
throw new
|
|
3411
|
+
throw new import_common13.MethodError("Invalid Beacon Type", "INVALID_BEACON_ERROR", service);
|
|
2631
3412
|
}
|
|
2632
3413
|
}
|
|
2633
3414
|
};
|
|
2634
3415
|
|
|
2635
3416
|
// src/core/beacon/signal-discovery.ts
|
|
2636
3417
|
var import_bitcoin2 = require("@did-btcr2/bitcoin");
|
|
2637
|
-
var
|
|
3418
|
+
var import_common16 = require("@did-btcr2/common");
|
|
2638
3419
|
|
|
2639
3420
|
// src/core/beacon/utils.ts
|
|
2640
3421
|
var import_bitcoin = require("@did-btcr2/bitcoin");
|
|
2641
|
-
var
|
|
2642
|
-
var
|
|
3422
|
+
var import_common15 = require("@did-btcr2/common");
|
|
3423
|
+
var import_btc_signer5 = require("@scure/btc-signer");
|
|
2643
3424
|
|
|
2644
3425
|
// src/utils/appendix.ts
|
|
2645
3426
|
var import_dids = require("@web5/dids");
|
|
@@ -3680,9 +4461,9 @@ var Appendix = class _Appendix {
|
|
|
3680
4461
|
};
|
|
3681
4462
|
|
|
3682
4463
|
// src/core/identifier.ts
|
|
3683
|
-
var
|
|
3684
|
-
var
|
|
3685
|
-
var
|
|
4464
|
+
var import_common14 = require("@did-btcr2/common");
|
|
4465
|
+
var import_keypair2 = require("@did-btcr2/keypair");
|
|
4466
|
+
var import_base7 = require("@scure/base");
|
|
3686
4467
|
var Identifier = class _Identifier {
|
|
3687
4468
|
/**
|
|
3688
4469
|
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-encoding | 3.2 did:btcr2 Identifier Encoding}.
|
|
@@ -3700,25 +4481,25 @@ var Identifier = class _Identifier {
|
|
|
3700
4481
|
*/
|
|
3701
4482
|
static encode(genesisBytes, options) {
|
|
3702
4483
|
const { idType, version = 1, network } = options;
|
|
3703
|
-
if (!(idType in
|
|
3704
|
-
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 });
|
|
3705
4486
|
}
|
|
3706
4487
|
if (isNaN(version) || version > 1) {
|
|
3707
|
-
throw new
|
|
4488
|
+
throw new import_common14.IdentifierError('Expected "version" to be 1', import_common14.INVALID_DID, { version });
|
|
3708
4489
|
}
|
|
3709
|
-
if (typeof network === "string" && !(network in
|
|
3710
|
-
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 });
|
|
3711
4492
|
}
|
|
3712
4493
|
if (typeof network === "number" && (network < 0 || network > 8)) {
|
|
3713
|
-
throw new
|
|
4494
|
+
throw new import_common14.IdentifierError('Invalid "network" number', import_common14.INVALID_DID, { network });
|
|
3714
4495
|
}
|
|
3715
4496
|
if (idType === "KEY") {
|
|
3716
4497
|
try {
|
|
3717
|
-
new
|
|
4498
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3718
4499
|
} catch {
|
|
3719
|
-
throw new
|
|
4500
|
+
throw new import_common14.IdentifierError(
|
|
3720
4501
|
'Expected "genesisBytes" to be a valid compressed secp256k1 public key',
|
|
3721
|
-
|
|
4502
|
+
import_common14.INVALID_DID,
|
|
3722
4503
|
{ genesisBytes }
|
|
3723
4504
|
);
|
|
3724
4505
|
}
|
|
@@ -3731,7 +4512,7 @@ var Identifier = class _Identifier {
|
|
|
3731
4512
|
}
|
|
3732
4513
|
nibbles.push((version - 1) % 15);
|
|
3733
4514
|
if (typeof network === "string") {
|
|
3734
|
-
nibbles.push(
|
|
4515
|
+
nibbles.push(import_common14.BitcoinNetworkNames[network]);
|
|
3735
4516
|
} else if (typeof network === "number") {
|
|
3736
4517
|
nibbles.push(network + 11);
|
|
3737
4518
|
}
|
|
@@ -3740,11 +4521,11 @@ var Identifier = class _Identifier {
|
|
|
3740
4521
|
}
|
|
3741
4522
|
if (fCount !== 0) {
|
|
3742
4523
|
for (const index in Array.from({ length: nibbles.length / 2 - 1 })) {
|
|
3743
|
-
throw new
|
|
4524
|
+
throw new import_common14.IdentifierError("Not implemented", "NOT_IMPLEMENTED", { index });
|
|
3744
4525
|
}
|
|
3745
4526
|
}
|
|
3746
4527
|
const dataBytes = new Uint8Array([nibbles[2 * 0] << 4 | nibbles[2 * 0 + 1], ...genesisBytes]);
|
|
3747
|
-
return `did:btcr2:${
|
|
4528
|
+
return `did:btcr2:${import_base7.bech32m.encodeFromBytes(hrp, dataBytes)}`;
|
|
3748
4529
|
}
|
|
3749
4530
|
/**
|
|
3750
4531
|
* Implements {@link https://dcdpr.github.io/did-btcr2/#didbtcr2-identifier-decoding | 3.3 did:btcr2 Identifier Decoding}.
|
|
@@ -3757,24 +4538,24 @@ var Identifier = class _Identifier {
|
|
|
3757
4538
|
static decode(identifier) {
|
|
3758
4539
|
const components = identifier.split(":");
|
|
3759
4540
|
if (components.length !== 3) {
|
|
3760
|
-
throw new
|
|
4541
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3761
4542
|
}
|
|
3762
4543
|
const [scheme, method, encoded] = components;
|
|
3763
4544
|
if (scheme !== "did") {
|
|
3764
|
-
throw new
|
|
4545
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3765
4546
|
}
|
|
3766
4547
|
if (method !== "btcr2") {
|
|
3767
|
-
throw new
|
|
4548
|
+
throw new import_common14.IdentifierError(`Invalid did method: ${method}`, import_common14.METHOD_NOT_SUPPORTED, { identifier });
|
|
3768
4549
|
}
|
|
3769
4550
|
if (!encoded) {
|
|
3770
|
-
throw new
|
|
4551
|
+
throw new import_common14.IdentifierError(`Invalid method-specific id: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3771
4552
|
}
|
|
3772
|
-
const { prefix: hrp, bytes: dataBytes } =
|
|
4553
|
+
const { prefix: hrp, bytes: dataBytes } = import_base7.bech32m.decodeToBytes(encoded);
|
|
3773
4554
|
if (!["x", "k"].includes(hrp)) {
|
|
3774
|
-
throw new
|
|
4555
|
+
throw new import_common14.IdentifierError(`Invalid hrp: ${hrp}`, import_common14.INVALID_DID, { identifier });
|
|
3775
4556
|
}
|
|
3776
4557
|
if (!dataBytes) {
|
|
3777
|
-
throw new
|
|
4558
|
+
throw new import_common14.IdentifierError(`Failed to decode id: ${encoded}`, import_common14.INVALID_DID, { identifier });
|
|
3778
4559
|
}
|
|
3779
4560
|
const idType = hrp === "k" ? "KEY" : "EXTERNAL";
|
|
3780
4561
|
let version = 1;
|
|
@@ -3792,33 +4573,33 @@ var Identifier = class _Identifier {
|
|
|
3792
4573
|
}
|
|
3793
4574
|
nibblesConsumed += 1;
|
|
3794
4575
|
if (version > 1) {
|
|
3795
|
-
throw new
|
|
4576
|
+
throw new import_common14.IdentifierError(`Invalid version: ${version}`, import_common14.INVALID_DID, { identifier });
|
|
3796
4577
|
}
|
|
3797
4578
|
}
|
|
3798
4579
|
version += versionNibble;
|
|
3799
4580
|
nibblesConsumed += 1;
|
|
3800
4581
|
let networkValue = nibblesConsumed % 2 === 0 ? dataBytes[++byteIndex] >>> 4 : currentByte & 15;
|
|
3801
4582
|
nibblesConsumed += 1;
|
|
3802
|
-
let network =
|
|
4583
|
+
let network = import_common14.BitcoinNetworkNames[networkValue];
|
|
3803
4584
|
if (!network) {
|
|
3804
4585
|
if (networkValue >= 8 && networkValue <= 15) {
|
|
3805
4586
|
network = networkValue - 11;
|
|
3806
4587
|
} else {
|
|
3807
|
-
throw new
|
|
4588
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3808
4589
|
}
|
|
3809
4590
|
}
|
|
3810
4591
|
if (nibblesConsumed % 2 === 1) {
|
|
3811
4592
|
const fillerNibble = currentByte & 15;
|
|
3812
4593
|
if (fillerNibble !== 0) {
|
|
3813
|
-
throw new
|
|
4594
|
+
throw new import_common14.IdentifierError(`Invalid did: ${identifier}`, import_common14.INVALID_DID, { identifier });
|
|
3814
4595
|
}
|
|
3815
4596
|
}
|
|
3816
4597
|
const genesisBytes = dataBytes.slice(byteIndex + 1);
|
|
3817
4598
|
if (idType === "KEY") {
|
|
3818
4599
|
try {
|
|
3819
|
-
new
|
|
4600
|
+
new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3820
4601
|
} catch {
|
|
3821
|
-
throw new
|
|
4602
|
+
throw new import_common14.IdentifierError(`Invalid genesisBytes: ${genesisBytes}`, import_common14.INVALID_DID, { identifier });
|
|
3822
4603
|
}
|
|
3823
4604
|
}
|
|
3824
4605
|
return { idType, hrp, version, network, genesisBytes };
|
|
@@ -3828,7 +4609,7 @@ var Identifier = class _Identifier {
|
|
|
3828
4609
|
* @returns {string} The new did:btcr2 identifier.
|
|
3829
4610
|
*/
|
|
3830
4611
|
static generate() {
|
|
3831
|
-
const keyPair =
|
|
4612
|
+
const keyPair = import_keypair2.SchnorrKeyPair.generate();
|
|
3832
4613
|
const did = this.encode(
|
|
3833
4614
|
keyPair.publicKey.compressed,
|
|
3834
4615
|
{
|
|
@@ -3848,13 +4629,13 @@ var Identifier = class _Identifier {
|
|
|
3848
4629
|
static getPublicKey(did) {
|
|
3849
4630
|
const { idType, genesisBytes } = _Identifier.decode(did);
|
|
3850
4631
|
if (idType !== "KEY") {
|
|
3851
|
-
throw new
|
|
4632
|
+
throw new import_common14.IdentifierError(
|
|
3852
4633
|
`Cannot extract public key from EXTERNAL DID: ${did}. EXTERNAL DIDs encode a document hash, not a public key.`,
|
|
3853
|
-
|
|
4634
|
+
import_common14.INVALID_DID,
|
|
3854
4635
|
{ did, idType }
|
|
3855
4636
|
);
|
|
3856
4637
|
}
|
|
3857
|
-
return new
|
|
4638
|
+
return new import_keypair2.CompressedSecp256k1PublicKey(genesisBytes);
|
|
3858
4639
|
}
|
|
3859
4640
|
/**
|
|
3860
4641
|
* Validates a did:btcr2 identifier.
|
|
@@ -3881,7 +4662,7 @@ var BeaconUtils = class {
|
|
|
3881
4662
|
*/
|
|
3882
4663
|
static parseBitcoinAddress(uri) {
|
|
3883
4664
|
if (!uri.startsWith("bitcoin:")) {
|
|
3884
|
-
throw new
|
|
4665
|
+
throw new import_common15.MethodError("Invalid Bitcoin URI format", "BEACON_SERVICE_ERROR", { uri });
|
|
3885
4666
|
}
|
|
3886
4667
|
return uri.replace("bitcoin:", "").split("?")[0];
|
|
3887
4668
|
}
|
|
@@ -3939,7 +4720,8 @@ var BeaconUtils = class {
|
|
|
3939
4720
|
const network = (0, import_bitcoin.getNetwork)(components.network);
|
|
3940
4721
|
const pubkey = components.genesisBytes;
|
|
3941
4722
|
const id = `${did}#initial${addressType.toUpperCase()}`;
|
|
3942
|
-
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}`;
|
|
3943
4725
|
return { id, type: beaconType, serviceEndpoint };
|
|
3944
4726
|
} catch (error) {
|
|
3945
4727
|
throw new BeaconError(
|
|
@@ -3957,27 +4739,27 @@ var BeaconUtils = class {
|
|
|
3957
4739
|
*/
|
|
3958
4740
|
static generateBeaconServices({ id, publicKey, network, beaconType }) {
|
|
3959
4741
|
try {
|
|
3960
|
-
const
|
|
3961
|
-
const
|
|
3962
|
-
const
|
|
3963
|
-
if (!
|
|
3964
|
-
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");
|
|
3965
4747
|
}
|
|
3966
4748
|
return [
|
|
3967
4749
|
{
|
|
3968
4750
|
id: `${id}#initialP2PKH`,
|
|
3969
4751
|
type: beaconType,
|
|
3970
|
-
serviceEndpoint: `bitcoin:${
|
|
4752
|
+
serviceEndpoint: `bitcoin:${p2pkhAddr}`
|
|
3971
4753
|
},
|
|
3972
4754
|
{
|
|
3973
4755
|
id: `${id}#initialP2WPKH`,
|
|
3974
4756
|
type: beaconType,
|
|
3975
|
-
serviceEndpoint: `bitcoin:${
|
|
4757
|
+
serviceEndpoint: `bitcoin:${p2wpkhAddr}`
|
|
3976
4758
|
},
|
|
3977
4759
|
{
|
|
3978
4760
|
id: `${id}#initialP2TR`,
|
|
3979
4761
|
type: beaconType,
|
|
3980
|
-
serviceEndpoint: `bitcoin:${
|
|
4762
|
+
serviceEndpoint: `bitcoin:${p2trAddr}`
|
|
3981
4763
|
}
|
|
3982
4764
|
];
|
|
3983
4765
|
} catch (error) {
|
|
@@ -4077,7 +4859,7 @@ var BeaconSignalDiscovery = class {
|
|
|
4077
4859
|
}
|
|
4078
4860
|
const rpc = bitcoin.rpc;
|
|
4079
4861
|
if (!rpc) {
|
|
4080
|
-
throw new
|
|
4862
|
+
throw new import_common16.ResolveError("RPC connection is not available", "RPC_CONNECTION_ERROR", bitcoin);
|
|
4081
4863
|
}
|
|
4082
4864
|
const targetHeight = await rpc.getBlockCount();
|
|
4083
4865
|
const beaconServicesMap = BeaconUtils.getBeaconServicesMap(beaconServices);
|
|
@@ -4148,26 +4930,24 @@ var BeaconSignalDiscovery = class {
|
|
|
4148
4930
|
|
|
4149
4931
|
// src/core/resolver.ts
|
|
4150
4932
|
var import_bitcoin4 = require("@did-btcr2/bitcoin");
|
|
4151
|
-
var
|
|
4152
|
-
var
|
|
4153
|
-
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");
|
|
4154
4936
|
|
|
4155
4937
|
// src/did-btcr2.ts
|
|
4156
|
-
var
|
|
4938
|
+
var import_common19 = require("@did-btcr2/common");
|
|
4157
4939
|
var import_dids2 = require("@web5/dids");
|
|
4158
|
-
var ecc = __toESM(require("@bitcoinerlab/secp256k1"), 1);
|
|
4159
|
-
var import_bitcoinjs_lib7 = require("bitcoinjs-lib");
|
|
4160
4940
|
|
|
4161
4941
|
// src/core/updater.ts
|
|
4162
|
-
var
|
|
4163
|
-
var
|
|
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,7 +5305,7 @@ 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
|
|
|
@@ -4572,19 +5352,19 @@ var Updater = class _Updater {
|
|
|
4572
5352
|
patch: patches,
|
|
4573
5353
|
targetHash: "",
|
|
4574
5354
|
targetVersionId: sourceVersionId + 1,
|
|
4575
|
-
sourceHash: (0,
|
|
5355
|
+
sourceHash: (0, import_common18.canonicalHash)(sourceDocument)
|
|
4576
5356
|
};
|
|
4577
|
-
const targetDocument =
|
|
5357
|
+
const targetDocument = import_common18.JSONPatch.apply(sourceDocument, patches);
|
|
4578
5358
|
try {
|
|
4579
5359
|
DidDocument.isValid(targetDocument);
|
|
4580
5360
|
} catch (error) {
|
|
4581
|
-
throw new
|
|
5361
|
+
throw new import_common18.UpdateError(
|
|
4582
5362
|
"Error validating targetDocument: " + (error instanceof Error ? error.message : String(error)),
|
|
4583
|
-
|
|
5363
|
+
import_common18.INVALID_DID_UPDATE,
|
|
4584
5364
|
targetDocument
|
|
4585
5365
|
);
|
|
4586
5366
|
}
|
|
4587
|
-
unsignedUpdate.targetHash = (0,
|
|
5367
|
+
unsignedUpdate.targetHash = (0, import_common18.canonicalHash)(targetDocument);
|
|
4588
5368
|
return unsignedUpdate;
|
|
4589
5369
|
}
|
|
4590
5370
|
/**
|
|
@@ -4599,7 +5379,7 @@ var Updater = class _Updater {
|
|
|
4599
5379
|
static sign(did, unsignedUpdate, verificationMethod, secretKey) {
|
|
4600
5380
|
const controller = verificationMethod.controller;
|
|
4601
5381
|
const id = verificationMethod.id.slice(verificationMethod.id.indexOf("#"));
|
|
4602
|
-
const multikey =
|
|
5382
|
+
const multikey = import_cryptosuite2.SchnorrMultikey.fromSecretKey(id, controller, secretKey);
|
|
4603
5383
|
const config = {
|
|
4604
5384
|
"@context": [
|
|
4605
5385
|
"https://w3id.org/security/v2",
|
|
@@ -4709,22 +5489,22 @@ var Updater = class _Updater {
|
|
|
4709
5489
|
switch (need.kind) {
|
|
4710
5490
|
case "NeedSigningKey": {
|
|
4711
5491
|
if (this.#phase !== "Sign" /* Sign */) {
|
|
4712
|
-
throw new
|
|
5492
|
+
throw new import_common18.UpdateError(
|
|
4713
5493
|
`Cannot provide NeedSigningKey: updater phase is ${this.#phase}, expected Sign.`,
|
|
4714
|
-
|
|
5494
|
+
import_common18.INVALID_DID_UPDATE,
|
|
4715
5495
|
{ phase: this.#phase }
|
|
4716
5496
|
);
|
|
4717
5497
|
}
|
|
4718
5498
|
if (!data) {
|
|
4719
|
-
throw new
|
|
5499
|
+
throw new import_common18.UpdateError(
|
|
4720
5500
|
"NeedSigningKey requires secret key bytes.",
|
|
4721
|
-
|
|
5501
|
+
import_common18.INVALID_DID_UPDATE
|
|
4722
5502
|
);
|
|
4723
5503
|
}
|
|
4724
5504
|
if (!this.#unsignedUpdate) {
|
|
4725
|
-
throw new
|
|
5505
|
+
throw new import_common18.UpdateError(
|
|
4726
5506
|
"Internal error: unsigned update missing in Sign phase.",
|
|
4727
|
-
|
|
5507
|
+
import_common18.INVALID_DID_UPDATE
|
|
4728
5508
|
);
|
|
4729
5509
|
}
|
|
4730
5510
|
this.#signedUpdate = this.#sign(data);
|
|
@@ -4733,9 +5513,9 @@ var Updater = class _Updater {
|
|
|
4733
5513
|
}
|
|
4734
5514
|
case "NeedFunding": {
|
|
4735
5515
|
if (this.#phase !== "Fund" /* Fund */) {
|
|
4736
|
-
throw new
|
|
5516
|
+
throw new import_common18.UpdateError(
|
|
4737
5517
|
`Cannot provide NeedFunding: updater phase is ${this.#phase}, expected Fund.`,
|
|
4738
|
-
|
|
5518
|
+
import_common18.INVALID_DID_UPDATE,
|
|
4739
5519
|
{ phase: this.#phase }
|
|
4740
5520
|
);
|
|
4741
5521
|
}
|
|
@@ -4744,9 +5524,9 @@ var Updater = class _Updater {
|
|
|
4744
5524
|
}
|
|
4745
5525
|
case "NeedBroadcast": {
|
|
4746
5526
|
if (this.#phase !== "Broadcast" /* Broadcast */) {
|
|
4747
|
-
throw new
|
|
5527
|
+
throw new import_common18.UpdateError(
|
|
4748
5528
|
`Cannot provide NeedBroadcast: updater phase is ${this.#phase}, expected Broadcast.`,
|
|
4749
|
-
|
|
5529
|
+
import_common18.INVALID_DID_UPDATE,
|
|
4750
5530
|
{ phase: this.#phase }
|
|
4751
5531
|
);
|
|
4752
5532
|
}
|
|
@@ -4758,7 +5538,6 @@ var Updater = class _Updater {
|
|
|
4758
5538
|
};
|
|
4759
5539
|
|
|
4760
5540
|
// src/did-btcr2.ts
|
|
4761
|
-
(0, import_bitcoinjs_lib7.initEccLib)(ecc);
|
|
4762
5541
|
var DidBtcr2 = class {
|
|
4763
5542
|
/**
|
|
4764
5543
|
* Name of the DID method, as defined in the DID BTCR2 specification
|
|
@@ -4783,9 +5562,9 @@ var DidBtcr2 = class {
|
|
|
4783
5562
|
static create(genesisBytes, options) {
|
|
4784
5563
|
const { idType, version = 1, network = "bitcoin" } = options || {};
|
|
4785
5564
|
if (!idType) {
|
|
4786
|
-
throw new
|
|
5565
|
+
throw new import_common19.MethodError(
|
|
4787
5566
|
"idType is required for creating a did:btcr2 identifier",
|
|
4788
|
-
|
|
5567
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4789
5568
|
options
|
|
4790
5569
|
);
|
|
4791
5570
|
}
|
|
@@ -4815,7 +5594,7 @@ var DidBtcr2 = class {
|
|
|
4815
5594
|
static resolve(did, resolutionOptions = {}) {
|
|
4816
5595
|
const didComponents = Identifier.decode(did);
|
|
4817
5596
|
const sidecarData = Resolver.sidecarData(resolutionOptions.sidecar);
|
|
4818
|
-
const currentDocument = didComponents.hrp ===
|
|
5597
|
+
const currentDocument = didComponents.hrp === import_common19.IdentifierHrp.k ? Resolver.deterministic(didComponents) : null;
|
|
4819
5598
|
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
4820
5599
|
versionId: resolutionOptions.versionId,
|
|
4821
5600
|
versionTime: resolutionOptions.versionTime,
|
|
@@ -4853,39 +5632,39 @@ var DidBtcr2 = class {
|
|
|
4853
5632
|
beaconId
|
|
4854
5633
|
}) {
|
|
4855
5634
|
if (!sourceDocument.capabilityInvocation?.some((vr) => vr === verificationMethodId)) {
|
|
4856
|
-
throw new
|
|
5635
|
+
throw new import_common19.UpdateError(
|
|
4857
5636
|
"Invalid verificationMethodId: not authorized for capabilityInvocation",
|
|
4858
|
-
|
|
5637
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4859
5638
|
sourceDocument
|
|
4860
5639
|
);
|
|
4861
5640
|
}
|
|
4862
5641
|
const verificationMethod = this.getSigningMethod(sourceDocument, verificationMethodId);
|
|
4863
5642
|
if (!verificationMethod) {
|
|
4864
|
-
throw new
|
|
5643
|
+
throw new import_common19.UpdateError(
|
|
4865
5644
|
"Invalid verificationMethod: not found in source document",
|
|
4866
|
-
|
|
5645
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4867
5646
|
{ sourceDocument, verificationMethodId }
|
|
4868
5647
|
);
|
|
4869
5648
|
}
|
|
4870
5649
|
if (verificationMethod.type !== "Multikey") {
|
|
4871
|
-
throw new
|
|
5650
|
+
throw new import_common19.UpdateError(
|
|
4872
5651
|
'Invalid verificationMethod: verificationMethod.type must be "Multikey"',
|
|
4873
|
-
|
|
5652
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4874
5653
|
verificationMethod
|
|
4875
5654
|
);
|
|
4876
5655
|
}
|
|
4877
5656
|
if (verificationMethod.publicKeyMultibase?.slice(0, 4) !== "zQ3s") {
|
|
4878
|
-
throw new
|
|
5657
|
+
throw new import_common19.UpdateError(
|
|
4879
5658
|
'Invalid verificationMethodId: publicKeyMultibase prefix must start with "zQ3s"',
|
|
4880
|
-
|
|
5659
|
+
import_common19.INVALID_DID_DOCUMENT,
|
|
4881
5660
|
verificationMethod
|
|
4882
5661
|
);
|
|
4883
5662
|
}
|
|
4884
5663
|
const beaconService = sourceDocument.service.filter((service) => service.id === beaconId).filter((service) => !!service).shift();
|
|
4885
5664
|
if (!beaconService) {
|
|
4886
|
-
throw new
|
|
5665
|
+
throw new import_common19.UpdateError(
|
|
4887
5666
|
"No beacon service found for provided beaconId",
|
|
4888
|
-
|
|
5667
|
+
import_common19.INVALID_DID_UPDATE,
|
|
4889
5668
|
{ sourceDocument, beaconId }
|
|
4890
5669
|
);
|
|
4891
5670
|
}
|
|
@@ -4911,7 +5690,7 @@ var DidBtcr2 = class {
|
|
|
4911
5690
|
methodId ??= "#initialKey";
|
|
4912
5691
|
const parsedDid = import_dids2.Did.parse(didDocument.id);
|
|
4913
5692
|
if (parsedDid && parsedDid.method !== this.methodName) {
|
|
4914
|
-
throw new
|
|
5693
|
+
throw new import_common19.MethodError(`Method not supported: ${parsedDid.method}`, import_common19.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
|
|
4915
5694
|
}
|
|
4916
5695
|
const verificationMethod = didDocument.verificationMethod?.find(
|
|
4917
5696
|
(vm) => Appendix.extractDidFragment(vm.id) === (Appendix.extractDidFragment(methodId) ?? Appendix.extractDidFragment(didDocument.assertionMethod?.[0]))
|
|
@@ -4967,7 +5746,7 @@ var Resolver = class _Resolver {
|
|
|
4967
5746
|
static deterministic(didComponents) {
|
|
4968
5747
|
const genesisBytes = didComponents.genesisBytes;
|
|
4969
5748
|
const did = Identifier.encode(genesisBytes, didComponents);
|
|
4970
|
-
const { multibase } = new
|
|
5749
|
+
const { multibase } = new import_keypair4.CompressedSecp256k1PublicKey(genesisBytes);
|
|
4971
5750
|
const service = BeaconUtils.generateBeaconServices({
|
|
4972
5751
|
id: did,
|
|
4973
5752
|
publicKey: genesisBytes,
|
|
@@ -4993,14 +5772,14 @@ var Resolver = class _Resolver {
|
|
|
4993
5772
|
* @throws {ResolveError} InvalidDidDocument if not conformant to DID Core v1.1
|
|
4994
5773
|
*/
|
|
4995
5774
|
static external(didComponents, genesisDocument) {
|
|
4996
|
-
const genesisDocumentHash = (0,
|
|
5775
|
+
const genesisDocumentHash = (0, import_common20.canonicalHashBytes)(genesisDocument);
|
|
4997
5776
|
if (!(0, import_utils10.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
|
|
4998
|
-
throw new
|
|
5777
|
+
throw new import_common20.ResolveError(
|
|
4999
5778
|
`Initial document mismatch: genesisBytes !== genesisDocumentHash`,
|
|
5000
|
-
|
|
5779
|
+
import_common20.INVALID_DID_DOCUMENT,
|
|
5001
5780
|
{
|
|
5002
|
-
genesisBytes: (0,
|
|
5003
|
-
genesisDocumentHash: (0,
|
|
5781
|
+
genesisBytes: (0, import_common20.encode)(didComponents.genesisBytes, "hex"),
|
|
5782
|
+
genesisDocumentHash: (0, import_common20.encode)(genesisDocumentHash, "hex")
|
|
5004
5783
|
}
|
|
5005
5784
|
);
|
|
5006
5785
|
}
|
|
@@ -5019,12 +5798,12 @@ var Resolver = class _Resolver {
|
|
|
5019
5798
|
const updateMap = /* @__PURE__ */ new Map();
|
|
5020
5799
|
if (sidecar.updates?.length)
|
|
5021
5800
|
for (const update of sidecar.updates) {
|
|
5022
|
-
updateMap.set((0,
|
|
5801
|
+
updateMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
5023
5802
|
}
|
|
5024
5803
|
const casMap = /* @__PURE__ */ new Map();
|
|
5025
5804
|
if (sidecar.casUpdates?.length)
|
|
5026
5805
|
for (const update of sidecar.casUpdates) {
|
|
5027
|
-
casMap.set((0,
|
|
5806
|
+
casMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
5028
5807
|
}
|
|
5029
5808
|
const smtMap = /* @__PURE__ */ new Map();
|
|
5030
5809
|
if (sidecar.smtProofs?.length)
|
|
@@ -5057,12 +5836,12 @@ var Resolver = class _Resolver {
|
|
|
5057
5836
|
}
|
|
5058
5837
|
};
|
|
5059
5838
|
for (const [update, block] of updates) {
|
|
5060
|
-
const currentDocumentHash = (0,
|
|
5061
|
-
const blocktime =
|
|
5062
|
-
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);
|
|
5063
5842
|
response.metadata.confirmations = block.confirmations;
|
|
5064
5843
|
if (versionTime) {
|
|
5065
|
-
if (blocktime >
|
|
5844
|
+
if (blocktime > import_common20.DateUtils.dateStringToTimestamp(versionTime)) {
|
|
5066
5845
|
return response;
|
|
5067
5846
|
}
|
|
5068
5847
|
}
|
|
@@ -5070,22 +5849,22 @@ var Resolver = class _Resolver {
|
|
|
5070
5849
|
updateHashHistory.push(currentDocumentHash);
|
|
5071
5850
|
this.confirmDuplicate(update, updateHashHistory);
|
|
5072
5851
|
} else if (update.targetVersionId === currentVersionId + 1) {
|
|
5073
|
-
const sourceHashBytes = (0,
|
|
5852
|
+
const sourceHashBytes = (0, import_common20.decode)(update.sourceHash, "base64urlnopad");
|
|
5074
5853
|
if (!(0, import_utils10.equalBytes)(sourceHashBytes, currentDocumentHash)) {
|
|
5075
|
-
throw new
|
|
5854
|
+
throw new import_common20.ResolveError(
|
|
5076
5855
|
`Hash mismatch: update.sourceHash !== currentDocumentHash`,
|
|
5077
|
-
|
|
5856
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5078
5857
|
{
|
|
5079
5858
|
sourceHash: update.sourceHash,
|
|
5080
|
-
currentDocumentHash: (0,
|
|
5859
|
+
currentDocumentHash: (0, import_common20.encode)(currentDocumentHash, "hex")
|
|
5081
5860
|
}
|
|
5082
5861
|
);
|
|
5083
5862
|
}
|
|
5084
5863
|
response.didDocument = this.applyUpdate(response.didDocument, update);
|
|
5085
|
-
const unsignedUpdate =
|
|
5086
|
-
updateHashHistory.push((0,
|
|
5864
|
+
const unsignedUpdate = import_common20.JSONUtils.deleteKeys(update, ["proof"]);
|
|
5865
|
+
updateHashHistory.push((0, import_common20.canonicalHashBytes)(unsignedUpdate));
|
|
5087
5866
|
} else if (update.targetVersionId > currentVersionId + 1) {
|
|
5088
|
-
throw new
|
|
5867
|
+
throw new import_common20.ResolveError(
|
|
5089
5868
|
`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
|
|
5090
5869
|
"LATE_PUBLISHING_ERROR",
|
|
5091
5870
|
{
|
|
@@ -5116,15 +5895,15 @@ var Resolver = class _Resolver {
|
|
|
5116
5895
|
*/
|
|
5117
5896
|
static confirmDuplicate(update, updateHashHistory) {
|
|
5118
5897
|
const { proof: _, ...unsignedUpdate } = update;
|
|
5119
|
-
const unsignedUpdateHash = (0,
|
|
5898
|
+
const unsignedUpdateHash = (0, import_common20.canonicalHashBytes)(unsignedUpdate);
|
|
5120
5899
|
const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
|
|
5121
5900
|
if (!(0, import_utils10.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
|
|
5122
|
-
throw new
|
|
5901
|
+
throw new import_common20.ResolveError(
|
|
5123
5902
|
`Invalid duplicate: unsigned update hash does not match historical hash`,
|
|
5124
|
-
|
|
5903
|
+
import_common20.LATE_PUBLISHING_ERROR,
|
|
5125
5904
|
{
|
|
5126
|
-
unsignedUpdateHash: (0,
|
|
5127
|
-
historicalHash: (0,
|
|
5905
|
+
unsignedUpdateHash: (0, import_common20.encode)(unsignedUpdateHash, "hex"),
|
|
5906
|
+
historicalHash: (0, import_common20.encode)(historicalUpdateHash, "hex")
|
|
5128
5907
|
}
|
|
5129
5908
|
);
|
|
5130
5909
|
}
|
|
@@ -5139,42 +5918,42 @@ var Resolver = class _Resolver {
|
|
|
5139
5918
|
static applyUpdate(currentDocument, update) {
|
|
5140
5919
|
const capabilityId = update.proof?.capability;
|
|
5141
5920
|
if (!capabilityId) {
|
|
5142
|
-
throw new
|
|
5921
|
+
throw new import_common20.ResolveError("No root capability found in update", import_common20.INVALID_DID_UPDATE, update);
|
|
5143
5922
|
}
|
|
5144
5923
|
const rootCapability = Appendix.dereferenceZcapId(capabilityId);
|
|
5145
5924
|
const { invocationTarget, controller: rootController } = rootCapability;
|
|
5146
5925
|
if (![invocationTarget, rootController].every((id) => id === currentDocument.id)) {
|
|
5147
|
-
throw new
|
|
5926
|
+
throw new import_common20.ResolveError(
|
|
5148
5927
|
"Invalid root capability",
|
|
5149
|
-
|
|
5928
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5150
5929
|
{ rootCapability, currentDocument }
|
|
5151
5930
|
);
|
|
5152
5931
|
}
|
|
5153
5932
|
const verificationMethodId = update.proof?.verificationMethod;
|
|
5154
5933
|
if (!verificationMethodId) {
|
|
5155
|
-
throw new
|
|
5934
|
+
throw new import_common20.ResolveError("No verificationMethod found in update", import_common20.INVALID_DID_UPDATE, update);
|
|
5156
5935
|
}
|
|
5157
5936
|
const vm = DidBtcr2.getSigningMethod(currentDocument, verificationMethodId);
|
|
5158
|
-
const multikey =
|
|
5159
|
-
const cryptosuite = new
|
|
5160
|
-
const canonicalUpdate = (0,
|
|
5161
|
-
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);
|
|
5162
5941
|
const verificationResult = diProof.verifyProof(canonicalUpdate, "capabilityInvocation");
|
|
5163
5942
|
if (!verificationResult.verified) {
|
|
5164
|
-
throw new
|
|
5943
|
+
throw new import_common20.ResolveError(
|
|
5165
5944
|
"Invalid update: proof not verified",
|
|
5166
|
-
|
|
5945
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5167
5946
|
verificationResult
|
|
5168
5947
|
);
|
|
5169
5948
|
}
|
|
5170
|
-
const updatedDocument =
|
|
5949
|
+
const updatedDocument = import_common20.JSONPatch.apply(currentDocument, update.patch);
|
|
5171
5950
|
DidDocument.validate(updatedDocument);
|
|
5172
|
-
const currentDocumentHash = (0,
|
|
5173
|
-
const updateTargetHash = (0,
|
|
5951
|
+
const currentDocumentHash = (0, import_common20.canonicalHashBytes)(updatedDocument);
|
|
5952
|
+
const updateTargetHash = (0, import_common20.decode)(update.targetHash);
|
|
5174
5953
|
if (!(0, import_utils10.equalBytes)(updateTargetHash, currentDocumentHash)) {
|
|
5175
|
-
throw new
|
|
5954
|
+
throw new import_common20.ResolveError(
|
|
5176
5955
|
`Invalid update: update.targetHash !== currentDocumentHash`,
|
|
5177
|
-
|
|
5956
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5178
5957
|
{ updateTargetHash, currentDocumentHash }
|
|
5179
5958
|
);
|
|
5180
5959
|
}
|
|
@@ -5202,7 +5981,7 @@ var Resolver = class _Resolver {
|
|
|
5202
5981
|
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
5203
5982
|
continue;
|
|
5204
5983
|
}
|
|
5205
|
-
const genesisHash = (0,
|
|
5984
|
+
const genesisHash = (0, import_common20.encode)(this.#didComponents.genesisBytes, "hex");
|
|
5206
5985
|
return {
|
|
5207
5986
|
status: "action-required",
|
|
5208
5987
|
needs: [{ kind: "NeedGenesisDocument", genesisHash }]
|
|
@@ -5306,21 +6085,21 @@ var Resolver = class _Resolver {
|
|
|
5306
6085
|
}
|
|
5307
6086
|
case "NeedCASAnnouncement": {
|
|
5308
6087
|
const announcement = data;
|
|
5309
|
-
this.#sidecarData.casMap.set((0,
|
|
6088
|
+
this.#sidecarData.casMap.set((0, import_common20.canonicalHash)(announcement, { encoding: "hex" }), announcement);
|
|
5310
6089
|
break;
|
|
5311
6090
|
}
|
|
5312
6091
|
case "NeedSignedUpdate": {
|
|
5313
6092
|
const update = data;
|
|
5314
|
-
this.#sidecarData.updateMap.set((0,
|
|
6093
|
+
this.#sidecarData.updateMap.set((0, import_common20.canonicalHash)(update, { encoding: "hex" }), update);
|
|
5315
6094
|
break;
|
|
5316
6095
|
}
|
|
5317
6096
|
case "NeedSMTProof": {
|
|
5318
6097
|
const smtNeed = need;
|
|
5319
6098
|
const proof = data;
|
|
5320
6099
|
if (proof.id !== smtNeed.smtRootHash) {
|
|
5321
|
-
throw new
|
|
6100
|
+
throw new import_common20.ResolveError(
|
|
5322
6101
|
`SMT proof root hash mismatch: expected ${smtNeed.smtRootHash}, got ${proof.id}`,
|
|
5323
|
-
|
|
6102
|
+
import_common20.INVALID_DID_UPDATE,
|
|
5324
6103
|
{ expected: smtNeed.smtRootHash, actual: proof.id }
|
|
5325
6104
|
);
|
|
5326
6105
|
}
|
|
@@ -5332,12 +6111,12 @@ var Resolver = class _Resolver {
|
|
|
5332
6111
|
};
|
|
5333
6112
|
|
|
5334
6113
|
// src/utils/did-document-builder.ts
|
|
5335
|
-
var
|
|
6114
|
+
var import_common21 = require("@did-btcr2/common");
|
|
5336
6115
|
var DidDocumentBuilder = class {
|
|
5337
6116
|
document = {};
|
|
5338
6117
|
constructor(initialDocument) {
|
|
5339
6118
|
if (!initialDocument.id) {
|
|
5340
|
-
throw new
|
|
6119
|
+
throw new import_common21.DidDocumentError('Missing required "id" property', import_common21.INVALID_DID_DOCUMENT, initialDocument);
|
|
5341
6120
|
}
|
|
5342
6121
|
this.document.id = initialDocument.id;
|
|
5343
6122
|
this.document.verificationMethod = initialDocument.verificationMethod ?? [];
|
|
@@ -5389,6 +6168,7 @@ var DidDocumentBuilder = class {
|
|
|
5389
6168
|
0 && (module.exports = {
|
|
5390
6169
|
AGGREGATED_NONCE,
|
|
5391
6170
|
AGGREGATION_MESSAGE_PREFIX,
|
|
6171
|
+
AGGREGATION_WIRE_VERSION,
|
|
5392
6172
|
AUTHORIZATION_REQUEST,
|
|
5393
6173
|
AggregateBeaconError,
|
|
5394
6174
|
AggregationCohort,
|
|
@@ -5415,6 +6195,10 @@ var DidDocumentBuilder = class {
|
|
|
5415
6195
|
COHORT_OPT_IN,
|
|
5416
6196
|
COHORT_OPT_IN_ACCEPT,
|
|
5417
6197
|
COHORT_READY,
|
|
6198
|
+
CONSOLE_LOGGER,
|
|
6199
|
+
DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
6200
|
+
DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
6201
|
+
DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
5418
6202
|
DEFAULT_NOSTR_RELAYS,
|
|
5419
6203
|
DID_REGEX,
|
|
5420
6204
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
@@ -5432,6 +6216,7 @@ var DidDocumentBuilder = class {
|
|
|
5432
6216
|
ParticipantCohortPhase,
|
|
5433
6217
|
Resolver,
|
|
5434
6218
|
SIGNATURE_AUTHORIZATION,
|
|
6219
|
+
SILENT_LOGGER,
|
|
5435
6220
|
SMTBeacon,
|
|
5436
6221
|
SMTBeaconError,
|
|
5437
6222
|
SUBMIT_UPDATE,
|
|
@@ -5447,6 +6232,7 @@ var DidDocumentBuilder = class {
|
|
|
5447
6232
|
TypedEventEmitter,
|
|
5448
6233
|
Updater,
|
|
5449
6234
|
VALIDATION_ACK,
|
|
6235
|
+
buildAggregationBeaconTx,
|
|
5450
6236
|
createAggregatedNonceMessage,
|
|
5451
6237
|
createAuthorizationRequestMessage,
|
|
5452
6238
|
createCohortAdvertMessage,
|
|
@@ -5458,8 +6244,22 @@ var DidDocumentBuilder = class {
|
|
|
5458
6244
|
createSignatureAuthorizationMessage,
|
|
5459
6245
|
createSubmitUpdateMessage,
|
|
5460
6246
|
createValidationAckMessage,
|
|
6247
|
+
getBeaconStrategy,
|
|
6248
|
+
isAggregatedNonceMessage,
|
|
5461
6249
|
isAggregationMessageType,
|
|
6250
|
+
isAuthorizationRequestMessage,
|
|
6251
|
+
isCohortAdvertMessage,
|
|
6252
|
+
isCohortOptInAcceptMessage,
|
|
6253
|
+
isCohortOptInMessage,
|
|
6254
|
+
isCohortReadyMessage,
|
|
6255
|
+
isDistributeAggregatedDataMessage,
|
|
5462
6256
|
isKeygenMessageType,
|
|
6257
|
+
isNonceContributionMessage,
|
|
5463
6258
|
isSignMessageType,
|
|
5464
|
-
|
|
6259
|
+
isSignatureAuthorizationMessage,
|
|
6260
|
+
isSubmitUpdateMessage,
|
|
6261
|
+
isUpdateMessageType,
|
|
6262
|
+
isValidationAckMessage,
|
|
6263
|
+
opReturnScript,
|
|
6264
|
+
registerBeaconStrategy
|
|
5465
6265
|
});
|