@did-btcr2/method 0.36.1 → 0.37.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 +1060 -96
- package/dist/browser.mjs +1060 -96
- package/dist/cjs/index.js +1153 -179
- package/dist/esm/core/aggregation/beacon-strategy.js +19 -6
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -1
- package/dist/esm/core/aggregation/cohort.js +142 -33
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/conditions.js +27 -0
- package/dist/esm/core/aggregation/conditions.js.map +1 -1
- package/dist/esm/core/aggregation/fallback-spend.js +132 -0
- package/dist/esm/core/aggregation/fallback-spend.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +46 -1
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
- package/dist/esm/core/aggregation/messages/constants.js +11 -0
- package/dist/esm/core/aggregation/messages/constants.js.map +1 -1
- package/dist/esm/core/aggregation/messages/factories.js +27 -1
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/guards.js +4 -1
- package/dist/esm/core/aggregation/messages/guards.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +210 -21
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/phases.js +6 -0
- package/dist/esm/core/aggregation/phases.js.map +1 -1
- package/dist/esm/core/aggregation/recovery-policy.js +156 -0
- package/dist/esm/core/aggregation/recovery-policy.js.map +1 -0
- package/dist/esm/core/aggregation/recovery-spend.js +106 -0
- package/dist/esm/core/aggregation/recovery-spend.js.map +1 -0
- package/dist/esm/core/aggregation/runner/aggregation-runner.js +11 -3
- package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +74 -22
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +97 -9
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +221 -11
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +3 -3
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/envelope.js +3 -3
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +1 -1
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +1 -1
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +1 -1
- package/dist/esm/core/aggregation/transport/http/request-auth.js +1 -1
- package/dist/esm/core/aggregation/transport/http/server.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +1 -1
- package/dist/esm/core/aggregation/transport/in-memory.js +1 -1
- package/dist/esm/core/aggregation/transport/nostr.js +6 -6
- package/dist/esm/core/beacon/beacon.js +15 -10
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +6 -6
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +2 -2
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +3 -3
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/resolver.js +8 -8
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +6 -6
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +2 -2
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +8 -4
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -1
- package/dist/types/core/aggregation/cohort.d.ts +88 -17
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/conditions.d.ts +28 -0
- package/dist/types/core/aggregation/conditions.d.ts.map +1 -1
- package/dist/types/core/aggregation/fallback-spend.d.ts +94 -0
- package/dist/types/core/aggregation/fallback-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/logger.d.ts +1 -1
- package/dist/types/core/aggregation/messages/base.d.ts +6 -0
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +50 -2
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/constants.d.ts +8 -0
- package/dist/types/core/aggregation/messages/constants.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/factories.d.ts +40 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/guards.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +41 -7
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/phases.d.ts +8 -2
- package/dist/types/core/aggregation/phases.d.ts.map +1 -1
- package/dist/types/core/aggregation/recovery-policy.d.ts +132 -0
- package/dist/types/core/aggregation/recovery-policy.d.ts.map +1 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts +94 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +13 -3
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +28 -7
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +3 -3
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +28 -3
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +26 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +2 -2
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +3 -3
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/server.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +2 -2
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/in-memory.d.ts +1 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +7 -7
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +2 -2
- package/dist/types/core/beacon/interfaces.d.ts +1 -1
- package/dist/types/core/resolver.d.ts +3 -3
- package/dist/types/core/updater.d.ts +4 -4
- package/dist/types/did-btcr2.d.ts +2 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/beacon-strategy.ts +27 -9
- package/src/core/aggregation/cohort.ts +171 -35
- package/src/core/aggregation/conditions.ts +56 -0
- package/src/core/aggregation/fallback-spend.ts +223 -0
- package/src/core/aggregation/logger.ts +1 -1
- package/src/core/aggregation/messages/base.ts +6 -0
- package/src/core/aggregation/messages/bodies.ts +94 -1
- package/src/core/aggregation/messages/constants.ts +12 -0
- package/src/core/aggregation/messages/factories.ts +57 -0
- package/src/core/aggregation/messages/guards.ts +6 -0
- package/src/core/aggregation/participant.ts +255 -23
- package/src/core/aggregation/phases.ts +9 -0
- package/src/core/aggregation/recovery-policy.ts +237 -0
- package/src/core/aggregation/recovery-spend.ts +192 -0
- package/src/core/aggregation/runner/aggregation-runner.ts +27 -5
- package/src/core/aggregation/runner/events.ts +26 -7
- package/src/core/aggregation/runner/participant-runner.ts +75 -23
- package/src/core/aggregation/runner/service-runner.ts +120 -10
- package/src/core/aggregation/service.ts +260 -14
- package/src/core/aggregation/signing-session.ts +3 -3
- package/src/core/aggregation/transport/http/envelope.ts +3 -3
- package/src/core/aggregation/transport/http/inbox-buffer.ts +1 -1
- package/src/core/aggregation/transport/http/nonce-cache.ts +1 -1
- package/src/core/aggregation/transport/http/rate-limiter.ts +1 -1
- package/src/core/aggregation/transport/http/request-auth.ts +1 -1
- package/src/core/aggregation/transport/http/server.ts +1 -1
- package/src/core/aggregation/transport/http/sse-stream.ts +2 -2
- package/src/core/aggregation/transport/http/sse-writer.ts +1 -1
- package/src/core/aggregation/transport/in-memory.ts +1 -1
- package/src/core/aggregation/transport/nostr.ts +6 -6
- package/src/core/aggregation/transport/transport.ts +4 -4
- package/src/core/beacon/beacon.ts +16 -11
- package/src/core/beacon/cas-beacon.ts +7 -7
- package/src/core/beacon/interfaces.ts +1 -1
- package/src/core/beacon/singleton-beacon.ts +2 -2
- package/src/core/beacon/smt-beacon.ts +3 -3
- package/src/core/resolver.ts +8 -8
- package/src/core/updater.ts +6 -6
- package/src/did-btcr2.ts +2 -2
- package/src/index.ts +3 -0
package/dist/browser.js
CHANGED
|
@@ -93180,9 +93180,11 @@ a=end-of-candidates
|
|
|
93180
93180
|
DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
93181
93181
|
DEFAULT_BROADCAST_LOOKBACK_MS: () => DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
93182
93182
|
DEFAULT_CLOCK_SKEW_SEC: () => DEFAULT_CLOCK_SKEW_SEC,
|
|
93183
|
+
DEFAULT_FUNDING_MODEL: () => DEFAULT_FUNDING_MODEL,
|
|
93183
93184
|
DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
93184
93185
|
DEFAULT_NONCE_LEN_BYTES: () => DEFAULT_NONCE_LEN_BYTES,
|
|
93185
93186
|
DEFAULT_NOSTR_RELAYS: () => DEFAULT_NOSTR_RELAYS,
|
|
93187
|
+
DEFAULT_RECOVERY_SEQUENCE: () => DEFAULT_RECOVERY_SEQUENCE,
|
|
93186
93188
|
DID_REGEX: () => DID_REGEX,
|
|
93187
93189
|
DISTRIBUTE_AGGREGATED_DATA: () => DISTRIBUTE_AGGREGATED_DATA,
|
|
93188
93190
|
DidBtcr2: () => DidBtcr2,
|
|
@@ -93191,6 +93193,8 @@ a=end-of-candidates
|
|
|
93191
93193
|
DidDocumentBuilder: () => DidDocumentBuilder,
|
|
93192
93194
|
DidVerificationMethod: () => DidVerificationMethod,
|
|
93193
93195
|
Document: () => Document,
|
|
93196
|
+
FALLBACK_AUTHORIZATION_REQUEST: () => FALLBACK_AUTHORIZATION_REQUEST,
|
|
93197
|
+
FALLBACK_SIGNATURE: () => FALLBACK_SIGNATURE,
|
|
93194
93198
|
GenesisDocument: () => GenesisDocument,
|
|
93195
93199
|
HTTP_ENVELOPE_VERSION: () => HTTP_ENVELOPE_VERSION,
|
|
93196
93200
|
HTTP_ROUTE: () => HTTP_ROUTE,
|
|
@@ -93204,7 +93208,9 @@ a=end-of-candidates
|
|
|
93204
93208
|
InMemoryTransport: () => InMemoryTransport,
|
|
93205
93209
|
InboxBuffer: () => InboxBuffer,
|
|
93206
93210
|
KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
|
|
93211
|
+
KNOWN_FUNDING_MODELS: () => KNOWN_FUNDING_MODELS,
|
|
93207
93212
|
KeyPairAggregationSigner: () => KeyPairAggregationSigner,
|
|
93213
|
+
MAX_RECOVERY_SEQUENCE: () => MAX_RECOVERY_SEQUENCE,
|
|
93208
93214
|
NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
|
|
93209
93215
|
NonceCache: () => NonceCache,
|
|
93210
93216
|
NostrTransport: () => NostrTransport,
|
|
@@ -93221,6 +93227,7 @@ a=end-of-candidates
|
|
|
93221
93227
|
SMTBeacon: () => SMTBeacon,
|
|
93222
93228
|
SMTBeaconError: () => SMTBeaconError,
|
|
93223
93229
|
SSE_EVENT: () => SSE_EVENT,
|
|
93230
|
+
SUBMIT_NONINCLUDED: () => SUBMIT_NONINCLUDED,
|
|
93224
93231
|
SUBMIT_UPDATE: () => SUBMIT_UPDATE,
|
|
93225
93232
|
ServiceCohortPhase: () => ServiceCohortPhase,
|
|
93226
93233
|
SigningSessionError: () => SigningSessionError,
|
|
@@ -93229,6 +93236,7 @@ a=end-of-candidates
|
|
|
93229
93236
|
SingletonBeacon: () => SingletonBeacon,
|
|
93230
93237
|
SingletonBeaconError: () => SingletonBeaconError,
|
|
93231
93238
|
StaticFeeEstimator: () => StaticFeeEstimator,
|
|
93239
|
+
TAPROOT_LEAF_VERSION: () => TAPROOT_LEAF_VERSION,
|
|
93232
93240
|
TransportAdapterError: () => TransportAdapterError,
|
|
93233
93241
|
TransportError: () => TransportError,
|
|
93234
93242
|
TransportFactory: () => TransportFactory,
|
|
@@ -93236,6 +93244,11 @@ a=end-of-candidates
|
|
|
93236
93244
|
Updater: () => Updater,
|
|
93237
93245
|
VALIDATION_ACK: () => VALIDATION_ACK,
|
|
93238
93246
|
buildAggregationBeaconTx: () => buildAggregationBeaconTx,
|
|
93247
|
+
buildFallbackLeaf: () => buildFallbackLeaf,
|
|
93248
|
+
buildFallbackSpend: () => buildFallbackSpend,
|
|
93249
|
+
buildRecoveryLeaves: () => buildRecoveryLeaves,
|
|
93250
|
+
buildRecoveryScript: () => buildRecoveryScript,
|
|
93251
|
+
buildRecoverySpend: () => buildRecoverySpend,
|
|
93239
93252
|
buildRequestAuth: () => buildRequestAuth,
|
|
93240
93253
|
createAggregatedNonceMessage: () => createAggregatedNonceMessage,
|
|
93241
93254
|
createAuthorizationRequestMessage: () => createAuthorizationRequestMessage,
|
|
@@ -93244,13 +93257,17 @@ a=end-of-candidates
|
|
|
93244
93257
|
createCohortOptInMessage: () => createCohortOptInMessage,
|
|
93245
93258
|
createCohortReadyMessage: () => createCohortReadyMessage,
|
|
93246
93259
|
createDistributeAggregatedDataMessage: () => createDistributeAggregatedDataMessage,
|
|
93260
|
+
createFallbackAuthorizationRequestMessage: () => createFallbackAuthorizationRequestMessage,
|
|
93261
|
+
createFallbackSignatureMessage: () => createFallbackSignatureMessage,
|
|
93247
93262
|
createNonceContributionMessage: () => createNonceContributionMessage,
|
|
93248
93263
|
createSignatureAuthorizationMessage: () => createSignatureAuthorizationMessage,
|
|
93264
|
+
createSubmitNonIncludedMessage: () => createSubmitNonIncludedMessage,
|
|
93249
93265
|
createSubmitUpdateMessage: () => createSubmitUpdateMessage,
|
|
93250
93266
|
createValidationAckMessage: () => createValidationAckMessage,
|
|
93251
93267
|
defaultReconnectBackoff: () => defaultReconnectBackoff,
|
|
93252
93268
|
deriveSingletonAddress: () => deriveSingletonAddress,
|
|
93253
93269
|
detectSingletonScriptKind: () => detectSingletonScriptKind,
|
|
93270
|
+
fallbackSighash: () => fallbackSighash,
|
|
93254
93271
|
formatSseComment: () => formatSseComment,
|
|
93255
93272
|
formatSseEvent: () => formatSseEvent,
|
|
93256
93273
|
getBeaconStrategy: () => getBeaconStrategy,
|
|
@@ -93262,10 +93279,13 @@ a=end-of-candidates
|
|
|
93262
93279
|
isCohortOptInMessage: () => isCohortOptInMessage,
|
|
93263
93280
|
isCohortReadyMessage: () => isCohortReadyMessage,
|
|
93264
93281
|
isDistributeAggregatedDataMessage: () => isDistributeAggregatedDataMessage,
|
|
93282
|
+
isFallbackAuthorizationRequestMessage: () => isFallbackAuthorizationRequestMessage,
|
|
93283
|
+
isFallbackSignatureMessage: () => isFallbackSignatureMessage,
|
|
93265
93284
|
isKeygenMessageType: () => isKeygenMessageType,
|
|
93266
93285
|
isNonceContributionMessage: () => isNonceContributionMessage,
|
|
93267
93286
|
isSignMessageType: () => isSignMessageType,
|
|
93268
93287
|
isSignatureAuthorizationMessage: () => isSignatureAuthorizationMessage,
|
|
93288
|
+
isSubmitNonIncludedMessage: () => isSubmitNonIncludedMessage,
|
|
93269
93289
|
isSubmitUpdateMessage: () => isSubmitUpdateMessage,
|
|
93270
93290
|
isUpdateMessageType: () => isUpdateMessageType,
|
|
93271
93291
|
isValidationAckMessage: () => isValidationAckMessage,
|
|
@@ -93274,8 +93294,10 @@ a=end-of-candidates
|
|
|
93274
93294
|
parseRequestAuth: () => parseRequestAuth,
|
|
93275
93295
|
parseSseStream: () => parseSseStream,
|
|
93276
93296
|
registerBeaconStrategy: () => registerBeaconStrategy,
|
|
93297
|
+
resolveFallbackThreshold: () => resolveFallbackThreshold,
|
|
93277
93298
|
reviveFromWire: () => reviveFromWire,
|
|
93278
93299
|
signEnvelope: () => signEnvelope,
|
|
93300
|
+
tapLeafHash: () => tapLeafHash2,
|
|
93279
93301
|
validateCohortConditions: () => validateCohortConditions,
|
|
93280
93302
|
verifyEnvelope: () => verifyEnvelope,
|
|
93281
93303
|
verifyRequestAuth: () => verifyRequestAuth
|
|
@@ -100261,7 +100283,7 @@ a=end-of-candidates
|
|
|
100261
100283
|
* @readonly
|
|
100262
100284
|
* Get signing ability of the Multikey: true if a local Secp256k1SecretKey
|
|
100263
100285
|
* is present (note: the SchnorrKeyPair.secretKey getter throws when absent;
|
|
100264
|
-
* that throw is the historical error contract
|
|
100286
|
+
* that throw is the historical error contract - see multikey tests) or if
|
|
100265
100287
|
* an external signer is available.
|
|
100266
100288
|
*/
|
|
100267
100289
|
get signer() {
|
|
@@ -100366,6 +100388,7 @@ a=end-of-candidates
|
|
|
100366
100388
|
init_shim();
|
|
100367
100389
|
|
|
100368
100390
|
// src/core/aggregation/service.ts
|
|
100391
|
+
init_secp256k1();
|
|
100369
100392
|
init_utils();
|
|
100370
100393
|
|
|
100371
100394
|
// src/core/aggregation/beacon-strategy.ts
|
|
@@ -100666,9 +100689,12 @@ a=end-of-candidates
|
|
|
100666
100689
|
getDistributePayload(cohort) {
|
|
100667
100690
|
return { casAnnouncement: cohort.casAnnouncement };
|
|
100668
100691
|
},
|
|
100669
|
-
validateParticipantView({ participantDid, expectedHash, body }) {
|
|
100692
|
+
validateParticipantView({ participantDid, included, expectedHash, body }) {
|
|
100670
100693
|
const casAnnouncement = body.casAnnouncement;
|
|
100671
100694
|
if (!casAnnouncement) return { matches: false };
|
|
100695
|
+
if (!included) {
|
|
100696
|
+
return { matches: !(participantDid in casAnnouncement), casAnnouncement };
|
|
100697
|
+
}
|
|
100672
100698
|
return {
|
|
100673
100699
|
matches: casAnnouncement[participantDid] === expectedHash,
|
|
100674
100700
|
casAnnouncement
|
|
@@ -100684,15 +100710,20 @@ a=end-of-candidates
|
|
|
100684
100710
|
const proof = cohort.smtProofs?.get(participantDid);
|
|
100685
100711
|
return { smtProof: proof };
|
|
100686
100712
|
},
|
|
100687
|
-
validateParticipantView({ participantDid, submittedUpdate, body }) {
|
|
100713
|
+
validateParticipantView({ participantDid, included, submittedUpdate, body }) {
|
|
100688
100714
|
const smtProof = body.smtProof;
|
|
100689
|
-
|
|
100715
|
+
const index = didToIndex(participantDid);
|
|
100716
|
+
if (!included) {
|
|
100717
|
+
if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
|
|
100718
|
+
const candidateHash2 = blockHash(blockHash(base64UrlToHash(smtProof.nonce)));
|
|
100719
|
+
return { matches: verifySerializedProof(smtProof, index, candidateHash2), smtProof };
|
|
100720
|
+
}
|
|
100721
|
+
if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
|
|
100690
100722
|
const canonicalBytes = new TextEncoder().encode(canonicalize2(submittedUpdate));
|
|
100691
100723
|
const expectedUpdateId = hashToBase64Url(blockHash(canonicalBytes));
|
|
100692
100724
|
if (smtProof.updateId !== expectedUpdateId) {
|
|
100693
100725
|
return { matches: false, smtProof };
|
|
100694
100726
|
}
|
|
100695
|
-
const index = didToIndex(participantDid);
|
|
100696
100727
|
const candidateHash = blockHash(blockHash(base64UrlToHash(smtProof.nonce)), base64UrlToHash(smtProof.updateId));
|
|
100697
100728
|
return {
|
|
100698
100729
|
matches: verifySerializedProof(smtProof, index, candidateHash),
|
|
@@ -101691,6 +101722,15 @@ a=end-of-candidates
|
|
|
101691
101722
|
checkWSH(r2, witnessScript);
|
|
101692
101723
|
}
|
|
101693
101724
|
}
|
|
101725
|
+
function uniqPubkey(pubkeys) {
|
|
101726
|
+
const map3 = {};
|
|
101727
|
+
for (const pub of pubkeys) {
|
|
101728
|
+
const key = hex.encode(pub);
|
|
101729
|
+
if (map3[key])
|
|
101730
|
+
throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
|
|
101731
|
+
map3[key] = true;
|
|
101732
|
+
}
|
|
101733
|
+
}
|
|
101694
101734
|
var p2pkh = (publicKey2, network = NETWORK) => {
|
|
101695
101735
|
if (!isValidPubkey(publicKey2, PubT.ecdsa))
|
|
101696
101736
|
throw new Error("P2PKH: invalid publicKey");
|
|
@@ -101863,6 +101903,14 @@ a=end-of-candidates
|
|
|
101863
101903
|
};
|
|
101864
101904
|
}
|
|
101865
101905
|
}
|
|
101906
|
+
function p2tr_ms(m2, pubkeys, allowSamePubkeys = false) {
|
|
101907
|
+
if (!allowSamePubkeys)
|
|
101908
|
+
uniqPubkey(pubkeys);
|
|
101909
|
+
return {
|
|
101910
|
+
type: "tr_ms",
|
|
101911
|
+
script: OutScript.encode({ type: "tr_ms", pubkeys, m: m2 })
|
|
101912
|
+
};
|
|
101913
|
+
}
|
|
101866
101914
|
var base58check = createBase58check(sha256);
|
|
101867
101915
|
function validateWitness(version3, data) {
|
|
101868
101916
|
if (data.length < 2 || data.length > 40)
|
|
@@ -103338,6 +103386,104 @@ a=end-of-candidates
|
|
|
103338
103386
|
}
|
|
103339
103387
|
};
|
|
103340
103388
|
|
|
103389
|
+
// src/core/aggregation/recovery-policy.ts
|
|
103390
|
+
init_shim();
|
|
103391
|
+
init_secp256k1();
|
|
103392
|
+
init_utils();
|
|
103393
|
+
var DEFAULT_FUNDING_MODEL = "operator-funded";
|
|
103394
|
+
var DEFAULT_RECOVERY_SEQUENCE = 144;
|
|
103395
|
+
var MAX_RECOVERY_SEQUENCE = 65535;
|
|
103396
|
+
var TAPROOT_LEAF_VERSION = 192;
|
|
103397
|
+
function compactSize(n2) {
|
|
103398
|
+
if (n2 < 253) return new Uint8Array([n2]);
|
|
103399
|
+
if (n2 <= 65535) return new Uint8Array([253, n2 & 255, n2 >> 8 & 255]);
|
|
103400
|
+
return new Uint8Array([254, n2 & 255, n2 >> 8 & 255, n2 >> 16 & 255, n2 >> 24 & 255]);
|
|
103401
|
+
}
|
|
103402
|
+
function tapLeafHash2(script, leafVersion = TAPROOT_LEAF_VERSION) {
|
|
103403
|
+
return schnorr2.utils.taggedHash(
|
|
103404
|
+
"TapLeaf",
|
|
103405
|
+
concatBytes(new Uint8Array([leafVersion]), compactSize(script.length), script)
|
|
103406
|
+
);
|
|
103407
|
+
}
|
|
103408
|
+
function resolveFallbackThreshold(advertised, n2) {
|
|
103409
|
+
return advertised ?? Math.max(1, n2 - 1);
|
|
103410
|
+
}
|
|
103411
|
+
function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
|
|
103412
|
+
const n2 = cohortKeys.length;
|
|
103413
|
+
if (n2 === 0) {
|
|
103414
|
+
throw new AggregationCohortError(
|
|
103415
|
+
"Cannot build fallback leaf: no cohort keys.",
|
|
103416
|
+
"NO_COHORT_KEYS"
|
|
103417
|
+
);
|
|
103418
|
+
}
|
|
103419
|
+
for (const key of cohortKeys) {
|
|
103420
|
+
if (key.length !== 33) {
|
|
103421
|
+
throw new AggregationCohortError(
|
|
103422
|
+
`Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
|
|
103423
|
+
"INVALID_COHORT_KEY",
|
|
103424
|
+
{ length: key.length }
|
|
103425
|
+
);
|
|
103426
|
+
}
|
|
103427
|
+
}
|
|
103428
|
+
if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n2) {
|
|
103429
|
+
throw new AggregationCohortError(
|
|
103430
|
+
`Fallback threshold must be an integer in [1, ${n2}] (k-of-n), got ${fallbackThreshold}.`,
|
|
103431
|
+
"INVALID_FALLBACK_THRESHOLD",
|
|
103432
|
+
{ fallbackThreshold, n: n2 }
|
|
103433
|
+
);
|
|
103434
|
+
}
|
|
103435
|
+
}
|
|
103436
|
+
function buildFallbackLeaf(params) {
|
|
103437
|
+
assertFallbackParams(params);
|
|
103438
|
+
const xOnlyKeys = sortKeys(params.cohortKeys).map((k) => k.slice(1));
|
|
103439
|
+
return p2tr_ms(params.fallbackThreshold, xOnlyKeys).script;
|
|
103440
|
+
}
|
|
103441
|
+
function assertRecoveryParams({ recoveryKey, recoverySequence }) {
|
|
103442
|
+
if (recoveryKey.length !== 32) {
|
|
103443
|
+
throw new AggregationCohortError(
|
|
103444
|
+
`Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
|
|
103445
|
+
"INVALID_RECOVERY_KEY"
|
|
103446
|
+
);
|
|
103447
|
+
}
|
|
103448
|
+
if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103449
|
+
throw new AggregationCohortError(
|
|
103450
|
+
`Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
|
|
103451
|
+
"INVALID_RECOVERY_SEQUENCE"
|
|
103452
|
+
);
|
|
103453
|
+
}
|
|
103454
|
+
}
|
|
103455
|
+
function buildRecoveryScript(params) {
|
|
103456
|
+
assertRecoveryParams(params);
|
|
103457
|
+
return Script.encode([
|
|
103458
|
+
params.recoverySequence,
|
|
103459
|
+
"CHECKSEQUENCEVERIFY",
|
|
103460
|
+
"DROP",
|
|
103461
|
+
params.recoveryKey,
|
|
103462
|
+
"CHECKSIG"
|
|
103463
|
+
]);
|
|
103464
|
+
}
|
|
103465
|
+
function buildRecoveryLeaves(fundingModel, params) {
|
|
103466
|
+
switch (fundingModel) {
|
|
103467
|
+
case "operator-funded":
|
|
103468
|
+
return [
|
|
103469
|
+
{ script: buildFallbackLeaf(params) },
|
|
103470
|
+
{ script: buildRecoveryScript(params) }
|
|
103471
|
+
];
|
|
103472
|
+
case "participant-funded":
|
|
103473
|
+
throw new AggregationCohortError(
|
|
103474
|
+
"Funding model 'participant-funded' is reserved and not yet implemented.",
|
|
103475
|
+
"UNSUPPORTED_FUNDING_MODEL",
|
|
103476
|
+
{ fundingModel }
|
|
103477
|
+
);
|
|
103478
|
+
default:
|
|
103479
|
+
throw new AggregationCohortError(
|
|
103480
|
+
`Unknown funding model: ${fundingModel}.`,
|
|
103481
|
+
"UNKNOWN_FUNDING_MODEL",
|
|
103482
|
+
{ fundingModel }
|
|
103483
|
+
);
|
|
103484
|
+
}
|
|
103485
|
+
}
|
|
103486
|
+
|
|
103341
103487
|
// src/core/aggregation/cohort.ts
|
|
103342
103488
|
var AggregationCohort = class {
|
|
103343
103489
|
/** Unique identifier for the cohort. */
|
|
@@ -103353,8 +103499,8 @@ a=end-of-candidates
|
|
|
103353
103499
|
/** List of participant DIDs that have been accepted into the cohort. */
|
|
103354
103500
|
participants = [];
|
|
103355
103501
|
/**
|
|
103356
|
-
* Mapping from participant DID
|
|
103357
|
-
* Distinct from {@link cohortKeys} (which is sorted per BIP-327)
|
|
103502
|
+
* Mapping from participant DID to their compressed secp256k1 public key.
|
|
103503
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
|
|
103358
103504
|
* callers look up a participant's key without knowing their position in the
|
|
103359
103505
|
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
103360
103506
|
*/
|
|
@@ -103362,16 +103508,41 @@ a=end-of-candidates
|
|
|
103362
103508
|
/** Sorted list of cohort participants' compressed public keys. */
|
|
103363
103509
|
#cohortKeys = [];
|
|
103364
103510
|
/**
|
|
103365
|
-
* BIP-341 TapTweak
|
|
103366
|
-
*
|
|
103367
|
-
*
|
|
103511
|
+
* BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
|
|
103512
|
+
* The beacon output is an internal key (the MuSig2 aggregate) plus a script
|
|
103513
|
+
* tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
|
|
103514
|
+
* The MuSig2 signing session applies this as an x-only tweak; a value that does
|
|
103515
|
+
* not match the root the funded address was derived from silently yields an
|
|
103516
|
+
* invalid key-path signature.
|
|
103368
103517
|
*/
|
|
103369
103518
|
tapTweak = new Uint8Array();
|
|
103370
|
-
/** The n-of-n MuSig2
|
|
103519
|
+
/** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
|
|
103520
|
+
internalKey = new Uint8Array();
|
|
103521
|
+
/** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
|
|
103522
|
+
tapMerkleRoot = new Uint8Array();
|
|
103523
|
+
/** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
|
|
103524
|
+
recoveryKey;
|
|
103525
|
+
/** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
|
|
103526
|
+
recoverySequence;
|
|
103527
|
+
/** Funding model governing the recovery leaves (default 'operator-funded'). */
|
|
103528
|
+
fundingModel;
|
|
103529
|
+
/**
|
|
103530
|
+
* Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
|
|
103531
|
+
* address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
|
|
103532
|
+
*/
|
|
103533
|
+
fallbackThreshold;
|
|
103534
|
+
/** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
|
|
103371
103535
|
beaconAddress = "";
|
|
103372
103536
|
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
103373
103537
|
pendingUpdates = /* @__PURE__ */ new Map();
|
|
103374
|
-
/**
|
|
103538
|
+
/**
|
|
103539
|
+
* Participant DIDs that explicitly declined to submit an update this round
|
|
103540
|
+
* (cooperative non-inclusion). A decliner is absent from the CAS Announcement
|
|
103541
|
+
* Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
|
|
103542
|
+
* disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
|
|
103543
|
+
*/
|
|
103544
|
+
nonIncluded = /* @__PURE__ */ new Set();
|
|
103545
|
+
/** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
|
|
103375
103546
|
casAnnouncement;
|
|
103376
103547
|
/** Per-participant SMT proofs, set by buildSMTTree(). */
|
|
103377
103548
|
smtProofs;
|
|
@@ -103381,12 +103552,16 @@ a=end-of-candidates
|
|
|
103381
103552
|
validationAcks = /* @__PURE__ */ new Set();
|
|
103382
103553
|
/** Set of participant DIDs that have rejected the aggregated data. */
|
|
103383
103554
|
validationRejections = /* @__PURE__ */ new Set();
|
|
103384
|
-
constructor({ id, minParticipants, serviceDid, network, beaconType }) {
|
|
103555
|
+
constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
|
|
103385
103556
|
this.id = id || crypto.randomUUID();
|
|
103386
103557
|
this.minParticipants = minParticipants ?? 2;
|
|
103387
103558
|
this.serviceDid = serviceDid || "";
|
|
103388
103559
|
this.network = network;
|
|
103389
103560
|
this.beaconType = beaconType || "CASBeacon";
|
|
103561
|
+
this.recoveryKey = recoveryKey;
|
|
103562
|
+
this.recoverySequence = recoverySequence;
|
|
103563
|
+
this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
|
|
103564
|
+
this.fallbackThreshold = fallbackThreshold;
|
|
103390
103565
|
}
|
|
103391
103566
|
/** Sorted cohort keys (sorted on assignment per BIP-327). */
|
|
103392
103567
|
get cohortKeys() {
|
|
@@ -103396,8 +103571,26 @@ a=end-of-candidates
|
|
|
103396
103571
|
this.#cohortKeys = sortKeys(keys);
|
|
103397
103572
|
}
|
|
103398
103573
|
/**
|
|
103399
|
-
*
|
|
103400
|
-
*
|
|
103574
|
+
* The resolved k of the k-of-n fallback leaf: the advertised
|
|
103575
|
+
* {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
|
|
103576
|
+
* the current cohort size. This is the value the beacon address commits to and
|
|
103577
|
+
* the spend builders must reproduce. Returns 0 before any cohort keys are set.
|
|
103578
|
+
*/
|
|
103579
|
+
get effectiveFallbackThreshold() {
|
|
103580
|
+
if (this.#cohortKeys.length === 0) return 0;
|
|
103581
|
+
return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
|
|
103582
|
+
}
|
|
103583
|
+
/**
|
|
103584
|
+
* Computes the Taproot beacon address from the cohort keys and recovery params.
|
|
103585
|
+
*
|
|
103586
|
+
* The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
|
|
103587
|
+
* script path is the recovery tree (a CSV recovery leaf so a missing signer
|
|
103588
|
+
* cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
|
|
103589
|
+
* the `tapTweak` the MuSig2 session must apply for the key-path spend.
|
|
103590
|
+
*
|
|
103591
|
+
* The tweak is derived from the Merkle root the address was built with (read
|
|
103592
|
+
* back from the payment), never recomputed by hand, so the MuSig2 key-path
|
|
103593
|
+
* signature is guaranteed to validate against the funded address.
|
|
103401
103594
|
*/
|
|
103402
103595
|
computeBeaconAddress() {
|
|
103403
103596
|
if (this.#cohortKeys.length === 0) {
|
|
@@ -103407,10 +103600,25 @@ a=end-of-candidates
|
|
|
103407
103600
|
{ cohortId: this.id }
|
|
103408
103601
|
);
|
|
103409
103602
|
}
|
|
103603
|
+
if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
|
|
103604
|
+
throw new AggregationCohortError(
|
|
103605
|
+
"Cannot compute beacon address: missing recovery key or sequence.",
|
|
103606
|
+
"NO_RECOVERY_PARAMS",
|
|
103607
|
+
{ cohortId: this.id }
|
|
103608
|
+
);
|
|
103609
|
+
}
|
|
103410
103610
|
const keyAggContext = keyAggregate(this.#cohortKeys);
|
|
103411
103611
|
const aggPubkey = keyAggExport(keyAggContext);
|
|
103412
|
-
const
|
|
103413
|
-
|
|
103612
|
+
const leaves = buildRecoveryLeaves(this.fundingModel, {
|
|
103613
|
+
recoveryKey: this.recoveryKey,
|
|
103614
|
+
recoverySequence: this.recoverySequence,
|
|
103615
|
+
cohortKeys: this.#cohortKeys,
|
|
103616
|
+
fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
|
|
103617
|
+
});
|
|
103618
|
+
const payment = p2tr(aggPubkey, leaves, getNetwork(this.network), true);
|
|
103619
|
+
this.internalKey = aggPubkey;
|
|
103620
|
+
this.tapMerkleRoot = payment.tapMerkleRoot;
|
|
103621
|
+
this.tapTweak = schnorr2.utils.taggedHash("TapTweak", concatBytes(aggPubkey, payment.tapMerkleRoot));
|
|
103414
103622
|
if (!payment.address) {
|
|
103415
103623
|
throw new AggregationCohortError(
|
|
103416
103624
|
"Failed to compute Taproot address",
|
|
@@ -103466,20 +103674,54 @@ a=end-of-candidates
|
|
|
103466
103674
|
}
|
|
103467
103675
|
this.pendingUpdates.set(participantDid, signedUpdate);
|
|
103468
103676
|
}
|
|
103677
|
+
/**
|
|
103678
|
+
* Record that a participant declined to submit an update this round
|
|
103679
|
+
* (cooperative non-inclusion). The member stays in the cohort and still signs.
|
|
103680
|
+
*/
|
|
103681
|
+
addNonInclusion(participantDid) {
|
|
103682
|
+
if (!this.participants.includes(participantDid)) {
|
|
103683
|
+
throw new AggregationCohortError(
|
|
103684
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
103685
|
+
"UNKNOWN_PARTICIPANT",
|
|
103686
|
+
{ cohortId: this.id, participantDid }
|
|
103687
|
+
);
|
|
103688
|
+
}
|
|
103689
|
+
if (this.pendingUpdates.has(participantDid)) {
|
|
103690
|
+
throw new AggregationCohortError(
|
|
103691
|
+
`Participant ${participantDid} already submitted an update; cannot also decline.`,
|
|
103692
|
+
"CONFLICTING_RESPONSE",
|
|
103693
|
+
{ cohortId: this.id, participantDid }
|
|
103694
|
+
);
|
|
103695
|
+
}
|
|
103696
|
+
this.nonIncluded.add(participantDid);
|
|
103697
|
+
}
|
|
103469
103698
|
hasAllUpdates() {
|
|
103470
103699
|
return this.pendingUpdates.size === this.participants.length;
|
|
103471
103700
|
}
|
|
103701
|
+
/**
|
|
103702
|
+
* True when every participant has responded for this round, either with an
|
|
103703
|
+
* update or with an explicit non-inclusion. This is the aggregation gate when
|
|
103704
|
+
* non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
|
|
103705
|
+
* {@link hasAllValidationResponses} generalizes a unanimous ack.
|
|
103706
|
+
*/
|
|
103707
|
+
hasAllResponses() {
|
|
103708
|
+
return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
|
|
103709
|
+
}
|
|
103472
103710
|
/**
|
|
103473
103711
|
* Builds a CAS Announcement Map from collected updates.
|
|
103474
|
-
* Maps each participant DID
|
|
103712
|
+
* Maps each participant DID to base64url canonical hash of their signed update.
|
|
103475
103713
|
* Computes signal bytes as SHA-256 of canonicalized announcement.
|
|
103714
|
+
*
|
|
103715
|
+
* Members who declined (cooperative non-inclusion) are naturally absent from
|
|
103716
|
+
* the map: the body iterates {@link pendingUpdates}, which never holds a
|
|
103717
|
+
* decliner. Absence from the map is exactly the CAS non-inclusion signal.
|
|
103476
103718
|
*/
|
|
103477
103719
|
buildCASAnnouncement() {
|
|
103478
|
-
if (!this.
|
|
103720
|
+
if (!this.hasAllResponses()) {
|
|
103479
103721
|
throw new AggregationCohortError(
|
|
103480
|
-
"Cannot build CAS Announcement: not all
|
|
103481
|
-
"
|
|
103482
|
-
{ cohortId: this.id,
|
|
103722
|
+
"Cannot build CAS Announcement: not all participants have responded.",
|
|
103723
|
+
"INCOMPLETE_RESPONSES",
|
|
103724
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103483
103725
|
);
|
|
103484
103726
|
}
|
|
103485
103727
|
const announcement = {};
|
|
@@ -103491,31 +103733,41 @@ a=end-of-candidates
|
|
|
103491
103733
|
return announcement;
|
|
103492
103734
|
}
|
|
103493
103735
|
/**
|
|
103494
|
-
* Builds an SMT tree
|
|
103495
|
-
*
|
|
103496
|
-
*
|
|
103736
|
+
* Builds an SMT tree with one leaf per participant.
|
|
103737
|
+
*
|
|
103738
|
+
* A member who submitted an update gets an inclusion leaf
|
|
103739
|
+
* (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
|
|
103740
|
+
* non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
|
|
103741
|
+
* omitted). The cohort mints each member's nonce and returns it inside that
|
|
103742
|
+
* member's serialized proof, so a decliner can self-validate its own
|
|
103743
|
+
* non-inclusion slot and the resolver can recompute the leaf. Stores
|
|
103744
|
+
* per-participant proofs and the SMT root as signalBytes.
|
|
103497
103745
|
*/
|
|
103498
103746
|
buildSMTTree() {
|
|
103499
|
-
if (!this.
|
|
103747
|
+
if (!this.hasAllResponses()) {
|
|
103500
103748
|
throw new AggregationCohortError(
|
|
103501
|
-
"Cannot build SMT tree: not all
|
|
103502
|
-
"
|
|
103503
|
-
{ cohortId: this.id }
|
|
103749
|
+
"Cannot build SMT tree: not all participants have responded.",
|
|
103750
|
+
"INCOMPLETE_RESPONSES",
|
|
103751
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103504
103752
|
);
|
|
103505
103753
|
}
|
|
103506
103754
|
const tree2 = new BTCR2MerkleTree();
|
|
103507
103755
|
const entries = [];
|
|
103508
103756
|
const encoder4 = new TextEncoder();
|
|
103509
|
-
for (const
|
|
103510
|
-
const canonicalBytes = encoder4.encode(canonicalize2(signedUpdate));
|
|
103757
|
+
for (const did of this.participants) {
|
|
103511
103758
|
const nonce = randomBytes(32);
|
|
103512
|
-
|
|
103759
|
+
const signedUpdate = this.pendingUpdates.get(did);
|
|
103760
|
+
if (signedUpdate) {
|
|
103761
|
+
entries.push({ did, nonce, signedUpdate: encoder4.encode(canonicalize2(signedUpdate)) });
|
|
103762
|
+
} else {
|
|
103763
|
+
entries.push({ did, nonce });
|
|
103764
|
+
}
|
|
103513
103765
|
}
|
|
103514
103766
|
tree2.addEntries(entries);
|
|
103515
103767
|
tree2.finalize();
|
|
103516
103768
|
this.signalBytes = tree2.rootHash;
|
|
103517
103769
|
this.smtProofs = /* @__PURE__ */ new Map();
|
|
103518
|
-
for (const
|
|
103770
|
+
for (const did of this.participants) {
|
|
103519
103771
|
this.smtProofs.set(did, tree2.proof(did));
|
|
103520
103772
|
}
|
|
103521
103773
|
return this.smtProofs;
|
|
@@ -103541,7 +103793,7 @@ a=end-of-candidates
|
|
|
103541
103793
|
return this.validationAcks.size + this.validationRejections.size === this.participants.length;
|
|
103542
103794
|
}
|
|
103543
103795
|
/**
|
|
103544
|
-
* True when all participants approved. Note: differs from {@link hasAllValidationResponses}
|
|
103796
|
+
* True when all participants approved. Note: differs from {@link hasAllValidationResponses},
|
|
103545
103797
|
* this returns false if any participant rejected, even if all responses are in.
|
|
103546
103798
|
*/
|
|
103547
103799
|
isFullyValidated() {
|
|
@@ -103552,6 +103804,7 @@ a=end-of-candidates
|
|
|
103552
103804
|
// src/core/aggregation/conditions.ts
|
|
103553
103805
|
init_shim();
|
|
103554
103806
|
var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
|
|
103807
|
+
var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
|
|
103555
103808
|
function checkPair(problems, label, min, max) {
|
|
103556
103809
|
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
103557
103810
|
problems.push(`min${label} must be an integer >= 0`);
|
|
@@ -103595,11 +103848,128 @@ a=end-of-candidates
|
|
|
103595
103848
|
if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
|
|
103596
103849
|
problems.push("pendingUpdateTrigger must be an integer >= 1");
|
|
103597
103850
|
}
|
|
103851
|
+
if (c2.fallbackThreshold !== void 0) {
|
|
103852
|
+
if (!Number.isInteger(c2.fallbackThreshold) || c2.fallbackThreshold < 1) {
|
|
103853
|
+
problems.push("fallbackThreshold must be an integer >= 1");
|
|
103854
|
+
} else if (c2.maxParticipants !== void 0 && Number.isInteger(c2.maxParticipants) && c2.fallbackThreshold > c2.maxParticipants) {
|
|
103855
|
+
problems.push("fallbackThreshold must be <= maxParticipants");
|
|
103856
|
+
}
|
|
103857
|
+
}
|
|
103598
103858
|
checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
|
|
103599
103859
|
checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
|
|
103860
|
+
if (typeof c2.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c2.recoveryKey)) {
|
|
103861
|
+
problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
|
|
103862
|
+
}
|
|
103863
|
+
if (!Number.isInteger(c2.recoverySequence) || c2.recoverySequence < 1 || c2.recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103864
|
+
problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
|
|
103865
|
+
}
|
|
103866
|
+
if (c2.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c2.fundingModel)) {
|
|
103867
|
+
problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
|
|
103868
|
+
}
|
|
103600
103869
|
return problems;
|
|
103601
103870
|
}
|
|
103602
103871
|
|
|
103872
|
+
// src/core/aggregation/fallback-spend.ts
|
|
103873
|
+
init_shim();
|
|
103874
|
+
init_secp256k1();
|
|
103875
|
+
function bytesEqual(a2, b) {
|
|
103876
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
103877
|
+
}
|
|
103878
|
+
function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
|
|
103879
|
+
return tx.preimageWitnessV1(
|
|
103880
|
+
inputIndex,
|
|
103881
|
+
[prevOutScript],
|
|
103882
|
+
SigHash.DEFAULT,
|
|
103883
|
+
[prevOutValue],
|
|
103884
|
+
void 0,
|
|
103885
|
+
fallbackLeafScript,
|
|
103886
|
+
TAPROOT_LEAF_VERSION
|
|
103887
|
+
);
|
|
103888
|
+
}
|
|
103889
|
+
function buildFallbackSpend(params) {
|
|
103890
|
+
const {
|
|
103891
|
+
pendingTx,
|
|
103892
|
+
cohortKeys,
|
|
103893
|
+
recoveryKey,
|
|
103894
|
+
recoverySequence,
|
|
103895
|
+
fundingModel,
|
|
103896
|
+
network,
|
|
103897
|
+
prevOutScript,
|
|
103898
|
+
prevOutValue,
|
|
103899
|
+
signatures
|
|
103900
|
+
} = params;
|
|
103901
|
+
const inputIndex = params.inputIndex ?? 0;
|
|
103902
|
+
if (cohortKeys.length === 0) {
|
|
103903
|
+
throw new AggregationCohortError(
|
|
103904
|
+
"Cannot build fallback spend: no cohort keys.",
|
|
103905
|
+
"NO_COHORT_KEYS"
|
|
103906
|
+
);
|
|
103907
|
+
}
|
|
103908
|
+
const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
|
|
103909
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
103910
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
103911
|
+
recoveryKey,
|
|
103912
|
+
recoverySequence,
|
|
103913
|
+
cohortKeys,
|
|
103914
|
+
fallbackThreshold
|
|
103915
|
+
});
|
|
103916
|
+
const net = getNetwork(network);
|
|
103917
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
103918
|
+
if (!bytesEqual(payment.script, prevOutScript)) {
|
|
103919
|
+
throw new AggregationCohortError(
|
|
103920
|
+
"Reconstructed beacon output script does not match the spent prevout script.",
|
|
103921
|
+
"PREVOUT_SCRIPT_MISMATCH"
|
|
103922
|
+
);
|
|
103923
|
+
}
|
|
103924
|
+
const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
|
|
103925
|
+
const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
|
|
103926
|
+
const script = scriptVer.slice(0, scriptVer.length - 1);
|
|
103927
|
+
return bytesEqual(script, fallbackLeaf);
|
|
103928
|
+
});
|
|
103929
|
+
if (!leafEntry) {
|
|
103930
|
+
throw new AggregationCohortError(
|
|
103931
|
+
"Could not locate the fallback leaf in the reconstructed beacon output.",
|
|
103932
|
+
"FALLBACK_LEAF_NOT_FOUND"
|
|
103933
|
+
);
|
|
103934
|
+
}
|
|
103935
|
+
const cohortXOnly = sortKeys(cohortKeys).map((k) => k.slice(1));
|
|
103936
|
+
const tx = pendingTx;
|
|
103937
|
+
const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
|
|
103938
|
+
const leafHash = tapLeafHash2(fallbackLeaf);
|
|
103939
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
103940
|
+
for (const { pubKey, signature } of signatures) {
|
|
103941
|
+
if (pubKey.length !== 32 || signature.length !== 64) continue;
|
|
103942
|
+
const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
|
|
103943
|
+
if (!isCohortKey) continue;
|
|
103944
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103945
|
+
if (accepted.has(hex2)) continue;
|
|
103946
|
+
let ok = false;
|
|
103947
|
+
try {
|
|
103948
|
+
ok = schnorr2.verify(signature, sighash, pubKey);
|
|
103949
|
+
} catch {
|
|
103950
|
+
ok = false;
|
|
103951
|
+
}
|
|
103952
|
+
if (ok) accepted.set(hex2, signature);
|
|
103953
|
+
}
|
|
103954
|
+
if (accepted.size < fallbackThreshold) {
|
|
103955
|
+
throw new AggregationCohortError(
|
|
103956
|
+
`Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
|
|
103957
|
+
"NOT_ENOUGH_FALLBACK_SIGNATURES",
|
|
103958
|
+
{ have: accepted.size, need: fallbackThreshold }
|
|
103959
|
+
);
|
|
103960
|
+
}
|
|
103961
|
+
const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
|
|
103962
|
+
tx.updateInput(inputIndex, {
|
|
103963
|
+
tapLeafScript: [leafEntry],
|
|
103964
|
+
tapScriptSig: chosen.map((pubKey) => {
|
|
103965
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103966
|
+
return [{ pubKey, leafHash }, accepted.get(hex2)];
|
|
103967
|
+
})
|
|
103968
|
+
});
|
|
103969
|
+
tx.finalize();
|
|
103970
|
+
return tx;
|
|
103971
|
+
}
|
|
103972
|
+
|
|
103603
103973
|
// src/core/aggregation/messages/base.ts
|
|
103604
103974
|
init_shim();
|
|
103605
103975
|
var AGGREGATION_WIRE_VERSION = 1;
|
|
@@ -103639,12 +104009,15 @@ a=end-of-candidates
|
|
|
103639
104009
|
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
103640
104010
|
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
103641
104011
|
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
104012
|
+
var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
|
|
103642
104013
|
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
103643
104014
|
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
103644
104015
|
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
103645
104016
|
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
103646
104017
|
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
103647
104018
|
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
104019
|
+
var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
|
|
104020
|
+
var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
|
|
103648
104021
|
|
|
103649
104022
|
// src/core/aggregation/messages/factories.ts
|
|
103650
104023
|
init_shim();
|
|
@@ -103668,6 +104041,10 @@ a=end-of-candidates
|
|
|
103668
104041
|
const { from: from8, to, ...body } = fields;
|
|
103669
104042
|
return new BaseMessage({ type: SUBMIT_UPDATE, from: from8, to, body });
|
|
103670
104043
|
}
|
|
104044
|
+
function createSubmitNonIncludedMessage(fields) {
|
|
104045
|
+
const { from: from8, to, ...body } = fields;
|
|
104046
|
+
return new BaseMessage({ type: SUBMIT_NONINCLUDED, from: from8, to, body });
|
|
104047
|
+
}
|
|
103671
104048
|
function createDistributeAggregatedDataMessage(fields) {
|
|
103672
104049
|
const { from: from8, to, ...body } = fields;
|
|
103673
104050
|
return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from: from8, to, body });
|
|
@@ -103692,6 +104069,14 @@ a=end-of-candidates
|
|
|
103692
104069
|
const { from: from8, to, ...body } = fields;
|
|
103693
104070
|
return new BaseMessage({ type: SIGNATURE_AUTHORIZATION, from: from8, to, body });
|
|
103694
104071
|
}
|
|
104072
|
+
function createFallbackAuthorizationRequestMessage(fields) {
|
|
104073
|
+
const { from: from8, to, ...body } = fields;
|
|
104074
|
+
return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from: from8, to, body });
|
|
104075
|
+
}
|
|
104076
|
+
function createFallbackSignatureMessage(fields) {
|
|
104077
|
+
const { from: from8, to, ...body } = fields;
|
|
104078
|
+
return new BaseMessage({ type: FALLBACK_SIGNATURE, from: from8, to, body });
|
|
104079
|
+
}
|
|
103695
104080
|
|
|
103696
104081
|
// src/core/aggregation/phases.ts
|
|
103697
104082
|
init_shim();
|
|
@@ -103706,6 +104091,7 @@ a=end-of-candidates
|
|
|
103706
104091
|
ServiceCohortPhase2["SigningStarted"] = "SigningStarted";
|
|
103707
104092
|
ServiceCohortPhase2["NoncesCollected"] = "NoncesCollected";
|
|
103708
104093
|
ServiceCohortPhase2["AwaitingPartialSigs"] = "AwaitingPartialSigs";
|
|
104094
|
+
ServiceCohortPhase2["FallbackRequested"] = "FallbackRequested";
|
|
103709
104095
|
ServiceCohortPhase2["Complete"] = "Complete";
|
|
103710
104096
|
ServiceCohortPhase2["Failed"] = "Failed";
|
|
103711
104097
|
return ServiceCohortPhase2;
|
|
@@ -103715,11 +104101,13 @@ a=end-of-candidates
|
|
|
103715
104101
|
ParticipantCohortPhase2["OptedIn"] = "OptedIn";
|
|
103716
104102
|
ParticipantCohortPhase2["CohortReady"] = "CohortReady";
|
|
103717
104103
|
ParticipantCohortPhase2["UpdateSubmitted"] = "UpdateSubmitted";
|
|
104104
|
+
ParticipantCohortPhase2["NonIncluded"] = "NonIncluded";
|
|
103718
104105
|
ParticipantCohortPhase2["AwaitingValidation"] = "AwaitingValidation";
|
|
103719
104106
|
ParticipantCohortPhase2["ValidationSent"] = "ValidationSent";
|
|
103720
104107
|
ParticipantCohortPhase2["AwaitingSigning"] = "AwaitingSigning";
|
|
103721
104108
|
ParticipantCohortPhase2["NonceSent"] = "NonceSent";
|
|
103722
104109
|
ParticipantCohortPhase2["AwaitingPartialSig"] = "AwaitingPartialSig";
|
|
104110
|
+
ParticipantCohortPhase2["AwaitingFallbackSig"] = "AwaitingFallbackSig";
|
|
103723
104111
|
ParticipantCohortPhase2["Complete"] = "Complete";
|
|
103724
104112
|
ParticipantCohortPhase2["Failed"] = "Failed";
|
|
103725
104113
|
return ParticipantCohortPhase2;
|
|
@@ -103747,11 +104135,11 @@ a=end-of-candidates
|
|
|
103747
104135
|
prevOutScripts;
|
|
103748
104136
|
/** Previous output values for Taproot sighash computation. */
|
|
103749
104137
|
prevOutValues;
|
|
103750
|
-
/** Map of participant publicKey-hex
|
|
104138
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
103751
104139
|
nonceContributions = /* @__PURE__ */ new Map();
|
|
103752
104140
|
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
103753
104141
|
aggregatedNonce;
|
|
103754
|
-
/** Map of participant DID
|
|
104142
|
+
/** Map of participant DID to partial signature. */
|
|
103755
104143
|
partialSignatures = /* @__PURE__ */ new Map();
|
|
103756
104144
|
/** Final 64-byte Schnorr signature. */
|
|
103757
104145
|
signature;
|
|
@@ -103934,7 +104322,7 @@ a=end-of-candidates
|
|
|
103934
104322
|
throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
|
|
103935
104323
|
}
|
|
103936
104324
|
if (!this.#secretNonce) {
|
|
103937
|
-
throw new SigningSessionError("Secret nonce not available
|
|
104325
|
+
throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
|
|
103938
104326
|
}
|
|
103939
104327
|
const session = new Session(
|
|
103940
104328
|
this.aggregatedNonce,
|
|
@@ -104004,6 +104392,9 @@ a=end-of-candidates
|
|
|
104004
104392
|
case SUBMIT_UPDATE:
|
|
104005
104393
|
this.#handleSubmitUpdate(message2);
|
|
104006
104394
|
break;
|
|
104395
|
+
case SUBMIT_NONINCLUDED:
|
|
104396
|
+
this.#handleSubmitNonInclusion(message2);
|
|
104397
|
+
break;
|
|
104007
104398
|
case VALIDATION_ACK:
|
|
104008
104399
|
this.#handleValidationAck(message2);
|
|
104009
104400
|
break;
|
|
@@ -104013,6 +104404,9 @@ a=end-of-candidates
|
|
|
104013
104404
|
case SIGNATURE_AUTHORIZATION:
|
|
104014
104405
|
this.#handleSignatureAuthorization(message2);
|
|
104015
104406
|
break;
|
|
104407
|
+
case FALLBACK_SIGNATURE:
|
|
104408
|
+
this.#handleFallbackSignature(message2);
|
|
104409
|
+
break;
|
|
104016
104410
|
default:
|
|
104017
104411
|
break;
|
|
104018
104412
|
}
|
|
@@ -104031,7 +104425,7 @@ a=end-of-candidates
|
|
|
104031
104425
|
}
|
|
104032
104426
|
/**
|
|
104033
104427
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
104034
|
-
* Cohort starts in `Created` phase
|
|
104428
|
+
* Cohort starts in `Created` phase - call `advertise()` to broadcast.
|
|
104035
104429
|
*/
|
|
104036
104430
|
createCohort(config) {
|
|
104037
104431
|
const problems = validateCohortConditions(config);
|
|
@@ -104046,7 +104440,11 @@ a=end-of-candidates
|
|
|
104046
104440
|
serviceDid: this.did,
|
|
104047
104441
|
minParticipants: config.minParticipants,
|
|
104048
104442
|
network: config.network,
|
|
104049
|
-
beaconType: config.beaconType
|
|
104443
|
+
beaconType: config.beaconType,
|
|
104444
|
+
recoveryKey: hexToBytes(config.recoveryKey),
|
|
104445
|
+
recoverySequence: config.recoverySequence,
|
|
104446
|
+
fundingModel: config.fundingModel,
|
|
104447
|
+
fallbackThreshold: config.fallbackThreshold
|
|
104050
104448
|
});
|
|
104051
104449
|
this.#cohortStates.set(cohort.id, {
|
|
104052
104450
|
phase: "Created" /* Created */,
|
|
@@ -104247,11 +104645,64 @@ a=end-of-candidates
|
|
|
104247
104645
|
});
|
|
104248
104646
|
return;
|
|
104249
104647
|
}
|
|
104648
|
+
if (state.cohort.nonIncluded.has(message2.from)) {
|
|
104649
|
+
state.rejections.push({
|
|
104650
|
+
from: message2.from,
|
|
104651
|
+
code: "UPDATE_MALFORMED",
|
|
104652
|
+
reason: "Participant already declined this round; cannot also submit an update"
|
|
104653
|
+
});
|
|
104654
|
+
return;
|
|
104655
|
+
}
|
|
104656
|
+
if (state.cohort.pendingUpdates.has(message2.from)) {
|
|
104657
|
+
state.rejections.push({
|
|
104658
|
+
from: message2.from,
|
|
104659
|
+
code: "UPDATE_MALFORMED",
|
|
104660
|
+
reason: "Participant already submitted an update this round; cannot resubmit"
|
|
104661
|
+
});
|
|
104662
|
+
return;
|
|
104663
|
+
}
|
|
104250
104664
|
state.cohort.addUpdate(message2.from, signedUpdate);
|
|
104251
104665
|
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104252
104666
|
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104253
104667
|
}
|
|
104254
|
-
if (state.cohort.
|
|
104668
|
+
if (state.cohort.hasAllResponses()) {
|
|
104669
|
+
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104670
|
+
}
|
|
104671
|
+
}
|
|
104672
|
+
/**
|
|
104673
|
+
* Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
|
|
104674
|
+
* update this round (cooperative non-inclusion). Membership is proven by the
|
|
104675
|
+
* signed transport envelope, so the body carries only the cohortId. The member
|
|
104676
|
+
* stays in the cohort and still signs; it is absent from the CAS map and
|
|
104677
|
+
* carries a non-inclusion leaf in the SMT.
|
|
104678
|
+
*/
|
|
104679
|
+
#handleSubmitNonInclusion(message2) {
|
|
104680
|
+
const cohortId = message2.body?.cohortId;
|
|
104681
|
+
if (!cohortId) return;
|
|
104682
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104683
|
+
if (!state) return;
|
|
104684
|
+
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
104685
|
+
if (!state.cohort.participants.includes(message2.from)) {
|
|
104686
|
+
state.rejections.push({
|
|
104687
|
+
from: message2.from,
|
|
104688
|
+
code: "UPDATE_MALFORMED",
|
|
104689
|
+
reason: "Sender is not a member of this cohort"
|
|
104690
|
+
});
|
|
104691
|
+
return;
|
|
104692
|
+
}
|
|
104693
|
+
if (state.cohort.pendingUpdates.has(message2.from) || state.cohort.nonIncluded.has(message2.from)) {
|
|
104694
|
+
state.rejections.push({
|
|
104695
|
+
from: message2.from,
|
|
104696
|
+
code: "UPDATE_MALFORMED",
|
|
104697
|
+
reason: "Participant already responded this round"
|
|
104698
|
+
});
|
|
104699
|
+
return;
|
|
104700
|
+
}
|
|
104701
|
+
state.cohort.addNonInclusion(message2.from);
|
|
104702
|
+
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104703
|
+
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104704
|
+
}
|
|
104705
|
+
if (state.cohort.hasAllResponses()) {
|
|
104255
104706
|
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104256
104707
|
}
|
|
104257
104708
|
}
|
|
@@ -104361,7 +104812,7 @@ a=end-of-candidates
|
|
|
104361
104812
|
}
|
|
104362
104813
|
/**
|
|
104363
104814
|
* Start a signing session by creating auth requests for all participants.
|
|
104364
|
-
* The caller provides the transaction data
|
|
104815
|
+
* The caller provides the transaction data - typically built via
|
|
104365
104816
|
* `buildBeaconTransaction()` against a Bitcoin connection.
|
|
104366
104817
|
*/
|
|
104367
104818
|
startSigning(cohortId, txData) {
|
|
@@ -104468,11 +104919,144 @@ a=end-of-candidates
|
|
|
104468
104919
|
state.result = {
|
|
104469
104920
|
cohortId,
|
|
104470
104921
|
signature,
|
|
104471
|
-
signedTx: state.signingSession.pendingTx
|
|
104922
|
+
signedTx: state.signingSession.pendingTx,
|
|
104923
|
+
path: "key-path"
|
|
104472
104924
|
};
|
|
104473
104925
|
state.phase = "Complete" /* Complete */;
|
|
104474
104926
|
}
|
|
104475
104927
|
}
|
|
104928
|
+
/**
|
|
104929
|
+
* Abandon the optimistic n-of-n key path and ask members to authorize the
|
|
104930
|
+
* k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
|
|
104931
|
+
* liveness, ADR 042). Reuses the in-flight signing session's transaction and
|
|
104932
|
+
* spent output, so the announcement and its outputs are unchanged: only the
|
|
104933
|
+
* witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
|
|
104934
|
+
* participant.
|
|
104935
|
+
*
|
|
104936
|
+
* Callable once optimistic signing has started (the session and its tx exist)
|
|
104937
|
+
* and before it completes. A cohort can take exactly one of the two paths: the
|
|
104938
|
+
* caller (runner) must commit to fallback and stop driving the optimistic path.
|
|
104939
|
+
*/
|
|
104940
|
+
startFallbackSigning(cohortId) {
|
|
104941
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104942
|
+
if (!state) {
|
|
104943
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
104944
|
+
}
|
|
104945
|
+
if (!state.signingSession) {
|
|
104946
|
+
throw new AggregationServiceError(
|
|
104947
|
+
`Cannot start fallback for cohort ${cohortId}: no signing session.`,
|
|
104948
|
+
"NO_SIGNING_SESSION",
|
|
104949
|
+
{ cohortId }
|
|
104950
|
+
);
|
|
104951
|
+
}
|
|
104952
|
+
const signingPhases = [
|
|
104953
|
+
"SigningStarted" /* SigningStarted */,
|
|
104954
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
104955
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
104956
|
+
];
|
|
104957
|
+
if (!signingPhases.includes(state.phase)) {
|
|
104958
|
+
throw new AggregationServiceError(
|
|
104959
|
+
`Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
104960
|
+
"INVALID_PHASE",
|
|
104961
|
+
{ cohortId, phase: state.phase }
|
|
104962
|
+
);
|
|
104963
|
+
}
|
|
104964
|
+
const session = state.signingSession;
|
|
104965
|
+
const prevOutScript = session.prevOutScripts[0];
|
|
104966
|
+
const prevOutValue = session.prevOutValues[0];
|
|
104967
|
+
if (!prevOutScript || prevOutValue === void 0) {
|
|
104968
|
+
throw new AggregationServiceError(
|
|
104969
|
+
`Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
|
|
104970
|
+
"MISSING_PREV_OUT",
|
|
104971
|
+
{ cohortId }
|
|
104972
|
+
);
|
|
104973
|
+
}
|
|
104974
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
104975
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
104976
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
104977
|
+
});
|
|
104978
|
+
state.fallbackSignatures = /* @__PURE__ */ new Map();
|
|
104979
|
+
state.phase = "FallbackRequested" /* FallbackRequested */;
|
|
104980
|
+
const messages2 = [];
|
|
104981
|
+
for (const participantDid of state.cohort.participants) {
|
|
104982
|
+
messages2.push(createFallbackAuthorizationRequestMessage({
|
|
104983
|
+
from: this.did,
|
|
104984
|
+
to: participantDid,
|
|
104985
|
+
cohortId,
|
|
104986
|
+
sessionId: session.id,
|
|
104987
|
+
pendingTx: session.pendingTx.hex,
|
|
104988
|
+
prevOutScriptHex: bytesToHex(prevOutScript),
|
|
104989
|
+
prevOutValue: prevOutValue.toString(),
|
|
104990
|
+
fallbackLeafScriptHex: bytesToHex(fallbackLeaf)
|
|
104991
|
+
}));
|
|
104992
|
+
}
|
|
104993
|
+
return messages2;
|
|
104994
|
+
}
|
|
104995
|
+
/**
|
|
104996
|
+
* Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
|
|
104997
|
+
* signature over the fallback script-path sighash. The signature is
|
|
104998
|
+
* authenticated to the sender (its `signerPk` must be the sender's own cohort
|
|
104999
|
+
* key and the signature must verify against the sighash) and collected. Once k
|
|
105000
|
+
* valid signatures are in, the k-of-n fallback spend is assembled and the
|
|
105001
|
+
* cohort completes via the script path.
|
|
105002
|
+
*/
|
|
105003
|
+
#handleFallbackSignature(message2) {
|
|
105004
|
+
const cohortId = message2.body?.cohortId;
|
|
105005
|
+
if (!cohortId) return;
|
|
105006
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105007
|
+
if (!state || !state.signingSession || !state.fallbackSignatures) return;
|
|
105008
|
+
if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
|
|
105009
|
+
const sessionId = message2.body?.sessionId;
|
|
105010
|
+
if (sessionId !== state.signingSession.id) return;
|
|
105011
|
+
const signerPk = message2.body?.signerPk;
|
|
105012
|
+
const fallbackSignature = message2.body?.fallbackSignature;
|
|
105013
|
+
if (!signerPk || !fallbackSignature) return;
|
|
105014
|
+
const prevOutScript = state.signingSession.prevOutScripts[0];
|
|
105015
|
+
const prevOutValue = state.signingSession.prevOutValues[0];
|
|
105016
|
+
if (!prevOutScript || prevOutValue === void 0) return;
|
|
105017
|
+
const memberKey = state.cohort.participantKeys.get(message2.from);
|
|
105018
|
+
if (!memberKey) {
|
|
105019
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
|
|
105020
|
+
return;
|
|
105021
|
+
}
|
|
105022
|
+
const memberXOnly = memberKey.slice(1);
|
|
105023
|
+
if (signerPk.length !== 32 || !memberXOnly.every((b, i4) => b === signerPk[i4])) {
|
|
105024
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
|
|
105025
|
+
return;
|
|
105026
|
+
}
|
|
105027
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
105028
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105029
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
105030
|
+
});
|
|
105031
|
+
const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
105032
|
+
let valid = false;
|
|
105033
|
+
try {
|
|
105034
|
+
valid = fallbackSignature.length === 64 && schnorr2.verify(fallbackSignature, sighash, signerPk);
|
|
105035
|
+
} catch {
|
|
105036
|
+
valid = false;
|
|
105037
|
+
}
|
|
105038
|
+
if (!valid) {
|
|
105039
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
|
|
105040
|
+
return;
|
|
105041
|
+
}
|
|
105042
|
+
state.fallbackSignatures.set(message2.from, { pubKey: signerPk, signature: fallbackSignature });
|
|
105043
|
+
if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
|
|
105044
|
+
const signedTx = buildFallbackSpend({
|
|
105045
|
+
pendingTx: state.signingSession.pendingTx,
|
|
105046
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105047
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold,
|
|
105048
|
+
recoveryKey: state.cohort.recoveryKey,
|
|
105049
|
+
recoverySequence: state.cohort.recoverySequence,
|
|
105050
|
+
fundingModel: state.cohort.fundingModel,
|
|
105051
|
+
network: state.cohort.network,
|
|
105052
|
+
prevOutScript,
|
|
105053
|
+
prevOutValue,
|
|
105054
|
+
signatures: [...state.fallbackSignatures.values()]
|
|
105055
|
+
});
|
|
105056
|
+
state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
|
|
105057
|
+
state.phase = "Complete" /* Complete */;
|
|
105058
|
+
}
|
|
105059
|
+
}
|
|
104476
105060
|
getResult(cohortId) {
|
|
104477
105061
|
return this.#cohortStates.get(cohortId)?.result;
|
|
104478
105062
|
}
|
|
@@ -104504,6 +105088,7 @@ a=end-of-candidates
|
|
|
104504
105088
|
|
|
104505
105089
|
// src/core/aggregation/participant.ts
|
|
104506
105090
|
init_shim();
|
|
105091
|
+
init_secp256k1();
|
|
104507
105092
|
init_utils();
|
|
104508
105093
|
|
|
104509
105094
|
// src/core/aggregation/messages/bodies.ts
|
|
@@ -104517,6 +105102,14 @@ a=end-of-candidates
|
|
|
104517
105102
|
const v = b ? b[k] : void 0;
|
|
104518
105103
|
return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
104519
105104
|
};
|
|
105105
|
+
var intInRange = (b, k, min, max) => {
|
|
105106
|
+
const v = b ? b[k] : void 0;
|
|
105107
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
105108
|
+
};
|
|
105109
|
+
var optStrOneOf = (b, k, allowed) => {
|
|
105110
|
+
const v = b ? b[k] : void 0;
|
|
105111
|
+
return v === void 0 || typeof v === "string" && allowed.includes(v);
|
|
105112
|
+
};
|
|
104520
105113
|
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
104521
105114
|
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
104522
105115
|
var hasBytesArray = (b, k) => {
|
|
@@ -104524,7 +105117,7 @@ a=end-of-candidates
|
|
|
104524
105117
|
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
104525
105118
|
};
|
|
104526
105119
|
function isCohortAdvertMessage(m2) {
|
|
104527
|
-
return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasBytes(m2.body, "communicationPk");
|
|
105120
|
+
return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasStr(m2.body, "recoveryKey") && intInRange(m2.body, "recoverySequence", 1, MAX_RECOVERY_SEQUENCE) && optStrOneOf(m2.body, "fundingModel", KNOWN_FUNDING_MODELS) && optIntMin(m2.body, "fallbackThreshold", 1) && hasBytes(m2.body, "communicationPk");
|
|
104528
105121
|
}
|
|
104529
105122
|
function isCohortOptInMessage(m2) {
|
|
104530
105123
|
return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
|
|
@@ -104538,6 +105131,9 @@ a=end-of-candidates
|
|
|
104538
105131
|
function isSubmitUpdateMessage(m2) {
|
|
104539
105132
|
return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
|
|
104540
105133
|
}
|
|
105134
|
+
function isSubmitNonIncludedMessage(m2) {
|
|
105135
|
+
return m2.type === SUBMIT_NONINCLUDED && hasStr(m2.body, "cohortId");
|
|
105136
|
+
}
|
|
104541
105137
|
function isDistributeAggregatedDataMessage(m2) {
|
|
104542
105138
|
return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
|
|
104543
105139
|
}
|
|
@@ -104556,8 +105152,38 @@ a=end-of-candidates
|
|
|
104556
105152
|
function isSignatureAuthorizationMessage(m2) {
|
|
104557
105153
|
return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
|
|
104558
105154
|
}
|
|
105155
|
+
function isFallbackAuthorizationRequestMessage(m2) {
|
|
105156
|
+
return m2.type === FALLBACK_AUTHORIZATION_REQUEST && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasStr(m2.body, "pendingTx") && hasStr(m2.body, "prevOutScriptHex") && hasStr(m2.body, "prevOutValue") && hasStr(m2.body, "fallbackLeafScriptHex");
|
|
105157
|
+
}
|
|
105158
|
+
function isFallbackSignatureMessage(m2) {
|
|
105159
|
+
return m2.type === FALLBACK_SIGNATURE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "signerPk") && hasBytes(m2.body, "fallbackSignature");
|
|
105160
|
+
}
|
|
104559
105161
|
|
|
104560
105162
|
// src/core/aggregation/participant.ts
|
|
105163
|
+
function txEmbedsSignal(tx, signalHex) {
|
|
105164
|
+
let expected;
|
|
105165
|
+
try {
|
|
105166
|
+
expected = hexToBytes(signalHex);
|
|
105167
|
+
} catch {
|
|
105168
|
+
return false;
|
|
105169
|
+
}
|
|
105170
|
+
if (expected.length === 0) return false;
|
|
105171
|
+
for (let i4 = 0; i4 < tx.outputsLength; i4++) {
|
|
105172
|
+
const script = tx.getOutput(i4)?.script;
|
|
105173
|
+
if (!script) continue;
|
|
105174
|
+
let decoded;
|
|
105175
|
+
try {
|
|
105176
|
+
decoded = Script.decode(script);
|
|
105177
|
+
} catch {
|
|
105178
|
+
continue;
|
|
105179
|
+
}
|
|
105180
|
+
if (decoded.length === 2 && decoded[0] === "RETURN" && decoded[1] instanceof Uint8Array) {
|
|
105181
|
+
const payload = decoded[1];
|
|
105182
|
+
if (payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
|
|
105183
|
+
}
|
|
105184
|
+
}
|
|
105185
|
+
return false;
|
|
105186
|
+
}
|
|
104561
105187
|
var AggregationParticipant = class {
|
|
104562
105188
|
did;
|
|
104563
105189
|
/** MuSig2 signing capability. The raw secret never lives as a field here. */
|
|
@@ -104574,7 +105200,7 @@ a=end-of-candidates
|
|
|
104574
105200
|
}
|
|
104575
105201
|
/**
|
|
104576
105202
|
* Process an incoming message. Updates internal state but never produces
|
|
104577
|
-
* outgoing messages
|
|
105203
|
+
* outgoing messages: those come exclusively from action methods.
|
|
104578
105204
|
*/
|
|
104579
105205
|
receive(message2) {
|
|
104580
105206
|
if (message2.version === void 0 || message2.version !== AGGREGATION_WIRE_VERSION) {
|
|
@@ -104600,6 +105226,9 @@ a=end-of-candidates
|
|
|
104600
105226
|
case AGGREGATED_NONCE:
|
|
104601
105227
|
this.#handleAggregatedNonce(message2);
|
|
104602
105228
|
break;
|
|
105229
|
+
case FALLBACK_AUTHORIZATION_REQUEST:
|
|
105230
|
+
this.#handleFallbackAuthorizationRequest(message2);
|
|
105231
|
+
break;
|
|
104603
105232
|
default:
|
|
104604
105233
|
break;
|
|
104605
105234
|
}
|
|
@@ -104645,12 +105274,17 @@ a=end-of-candidates
|
|
|
104645
105274
|
{ cohortId, phase: state?.phase }
|
|
104646
105275
|
);
|
|
104647
105276
|
}
|
|
105277
|
+
const advert = state.advert;
|
|
104648
105278
|
const cohort = new AggregationCohort({
|
|
104649
105279
|
id: cohortId,
|
|
104650
105280
|
serviceDid: state.serviceDid,
|
|
104651
|
-
minParticipants:
|
|
104652
|
-
network:
|
|
104653
|
-
beaconType:
|
|
105281
|
+
minParticipants: advert.minParticipants,
|
|
105282
|
+
network: advert.network,
|
|
105283
|
+
beaconType: advert.beaconType,
|
|
105284
|
+
recoveryKey: advert.recoveryKey ? hexToBytes(advert.recoveryKey) : void 0,
|
|
105285
|
+
recoverySequence: advert.recoverySequence,
|
|
105286
|
+
fundingModel: advert.fundingModel,
|
|
105287
|
+
fallbackThreshold: advert.fallbackThreshold
|
|
104654
105288
|
});
|
|
104655
105289
|
state.cohort = cohort;
|
|
104656
105290
|
state.phase = "OptedIn" /* OptedIn */;
|
|
@@ -104665,7 +105299,7 @@ a=end-of-candidates
|
|
|
104665
105299
|
}
|
|
104666
105300
|
#handleOptInAccept(message2) {
|
|
104667
105301
|
}
|
|
104668
|
-
/** Cohorts that have been finalized
|
|
105302
|
+
/** Cohorts that have been finalized: beacon address available. */
|
|
104669
105303
|
get joinedCohorts() {
|
|
104670
105304
|
const map3 = /* @__PURE__ */ new Map();
|
|
104671
105305
|
for (const [id, state] of this.#cohortStates) {
|
|
@@ -104708,6 +105342,7 @@ a=end-of-candidates
|
|
|
104708
105342
|
);
|
|
104709
105343
|
}
|
|
104710
105344
|
state.submittedUpdate = signedUpdate;
|
|
105345
|
+
state.included = true;
|
|
104711
105346
|
state.phase = "UpdateSubmitted" /* UpdateSubmitted */;
|
|
104712
105347
|
const message2 = createSubmitUpdateMessage({
|
|
104713
105348
|
from: this.did,
|
|
@@ -104717,6 +105352,30 @@ a=end-of-candidates
|
|
|
104717
105352
|
});
|
|
104718
105353
|
return [message2];
|
|
104719
105354
|
}
|
|
105355
|
+
/**
|
|
105356
|
+
* User action: decline to submit an update this round (cooperative
|
|
105357
|
+
* non-inclusion). The member stays in the cohort and still signs; it will be
|
|
105358
|
+
* absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
|
|
105359
|
+
* SMT. Returns the SUBMIT_NONINCLUDED message to send.
|
|
105360
|
+
*/
|
|
105361
|
+
declineUpdate(cohortId) {
|
|
105362
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105363
|
+
if (!state || state.phase !== "CohortReady" /* CohortReady */) {
|
|
105364
|
+
throw new AggregationParticipantError(
|
|
105365
|
+
`Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
|
|
105366
|
+
"INVALID_PHASE",
|
|
105367
|
+
{ cohortId, phase: state?.phase }
|
|
105368
|
+
);
|
|
105369
|
+
}
|
|
105370
|
+
state.included = false;
|
|
105371
|
+
state.phase = "NonIncluded" /* NonIncluded */;
|
|
105372
|
+
const message2 = createSubmitNonIncludedMessage({
|
|
105373
|
+
from: this.did,
|
|
105374
|
+
to: state.serviceDid,
|
|
105375
|
+
cohortId
|
|
105376
|
+
});
|
|
105377
|
+
return [message2];
|
|
105378
|
+
}
|
|
104720
105379
|
/** Aggregated data awaiting user validation. */
|
|
104721
105380
|
get pendingValidations() {
|
|
104722
105381
|
const map3 = /* @__PURE__ */ new Map();
|
|
@@ -104730,9 +105389,9 @@ a=end-of-candidates
|
|
|
104730
105389
|
/**
|
|
104731
105390
|
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
104732
105391
|
* Unlike {@link pendingValidations} (which lists only cohorts still awaiting
|
|
104733
|
-
* the validate decision), this returns the stored validation
|
|
104734
|
-
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
|
|
104735
|
-
*
|
|
105392
|
+
* the validate decision), this returns the stored validation, including the
|
|
105393
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
|
|
105394
|
+
* so it is still readable once the cohort reaches Complete. Returns
|
|
104736
105395
|
* undefined before aggregated data has been received.
|
|
104737
105396
|
*/
|
|
104738
105397
|
getValidation(cohortId) {
|
|
@@ -104742,18 +105401,20 @@ a=end-of-candidates
|
|
|
104742
105401
|
const cohortId = message2.body?.cohortId;
|
|
104743
105402
|
if (!cohortId) return;
|
|
104744
105403
|
const state = this.#cohortStates.get(cohortId);
|
|
104745
|
-
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
|
|
104746
|
-
|
|
105404
|
+
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */ && state.phase !== "NonIncluded" /* NonIncluded */) return;
|
|
105405
|
+
const declined = state.included === false;
|
|
105406
|
+
if (!declined && !state.submittedUpdate) return;
|
|
104747
105407
|
const beaconType = message2.body?.beaconType;
|
|
104748
105408
|
if (!beaconType) return;
|
|
104749
105409
|
const strategy = getBeaconStrategy(beaconType);
|
|
104750
105410
|
if (!strategy) return;
|
|
104751
105411
|
const signalBytesHex = message2.body?.signalBytesHex ?? "";
|
|
104752
|
-
const expectedHash = canonicalHash(state.submittedUpdate);
|
|
105412
|
+
const expectedHash = declined ? "" : canonicalHash(state.submittedUpdate);
|
|
104753
105413
|
const result = strategy.validateParticipantView({
|
|
104754
105414
|
participantDid: this.did,
|
|
104755
|
-
|
|
104756
|
-
|
|
105415
|
+
included: !declined,
|
|
105416
|
+
submittedUpdate: declined ? void 0 : state.submittedUpdate,
|
|
105417
|
+
expectedHash: declined ? void 0 : expectedHash,
|
|
104757
105418
|
body: message2.body
|
|
104758
105419
|
});
|
|
104759
105420
|
state.validation = {
|
|
@@ -104763,7 +105424,8 @@ a=end-of-candidates
|
|
|
104763
105424
|
expectedHash,
|
|
104764
105425
|
matches: result.matches,
|
|
104765
105426
|
casAnnouncement: result.casAnnouncement,
|
|
104766
|
-
smtProof: result.smtProof
|
|
105427
|
+
smtProof: result.smtProof,
|
|
105428
|
+
included: !declined
|
|
104767
105429
|
};
|
|
104768
105430
|
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
104769
105431
|
}
|
|
@@ -104837,6 +105499,31 @@ a=end-of-candidates
|
|
|
104837
105499
|
};
|
|
104838
105500
|
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
104839
105501
|
}
|
|
105502
|
+
/**
|
|
105503
|
+
* Bind a signing approval to the announcement the member validated: a beacon
|
|
105504
|
+
* transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
|
|
105505
|
+
* the aggregated data was distributed. Both the optimistic nonce approval and
|
|
105506
|
+
* the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
|
|
105507
|
+
* while the coordinator drives output selection, so without this check a
|
|
105508
|
+
* coordinator could anchor a different signal under the member's signature.
|
|
105509
|
+
*/
|
|
105510
|
+
#assertTxAnchorsValidatedSignal(cohortId, state, tx) {
|
|
105511
|
+
const signalHex = state.validation?.signalBytesHex;
|
|
105512
|
+
if (!signalHex) {
|
|
105513
|
+
throw new AggregationParticipantError(
|
|
105514
|
+
`Cohort ${cohortId} has no validated signal to bind the signature to.`,
|
|
105515
|
+
"MISSING_STATE",
|
|
105516
|
+
{ cohortId }
|
|
105517
|
+
);
|
|
105518
|
+
}
|
|
105519
|
+
if (!txEmbedsSignal(tx, signalHex)) {
|
|
105520
|
+
throw new AggregationParticipantError(
|
|
105521
|
+
`Transaction for cohort ${cohortId} does not anchor the validated signal.`,
|
|
105522
|
+
"SIGNAL_MISMATCH",
|
|
105523
|
+
{ cohortId }
|
|
105524
|
+
);
|
|
105525
|
+
}
|
|
105526
|
+
}
|
|
104840
105527
|
/**
|
|
104841
105528
|
* User action: approve signing and generate nonce contribution.
|
|
104842
105529
|
*/
|
|
@@ -104856,7 +105543,8 @@ a=end-of-candidates
|
|
|
104856
105543
|
{ cohortId }
|
|
104857
105544
|
);
|
|
104858
105545
|
}
|
|
104859
|
-
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
|
|
105546
|
+
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
|
|
105547
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
104860
105548
|
const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
|
|
104861
105549
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
104862
105550
|
const session = new BeaconSigningSession({
|
|
@@ -104892,7 +105580,7 @@ a=end-of-candidates
|
|
|
104892
105580
|
}
|
|
104893
105581
|
/**
|
|
104894
105582
|
* User action: generate and return the partial signature.
|
|
104895
|
-
* In most UIs this is automatic after AwaitingPartialSig
|
|
105583
|
+
* In most UIs this is automatic after AwaitingPartialSig, but exposing it
|
|
104896
105584
|
* as an explicit action lets the client UI confirm before signing if desired.
|
|
104897
105585
|
*/
|
|
104898
105586
|
generatePartialSignature(cohortId) {
|
|
@@ -104924,6 +105612,83 @@ a=end-of-candidates
|
|
|
104924
105612
|
partialSignature: partialSig
|
|
104925
105613
|
})];
|
|
104926
105614
|
}
|
|
105615
|
+
/** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
|
|
105616
|
+
get pendingFallbackRequests() {
|
|
105617
|
+
const map3 = /* @__PURE__ */ new Map();
|
|
105618
|
+
for (const [id, state] of this.#cohortStates) {
|
|
105619
|
+
if (state.phase === "AwaitingFallbackSig" /* AwaitingFallbackSig */ && state.fallbackRequest) {
|
|
105620
|
+
map3.set(id, state.fallbackRequest);
|
|
105621
|
+
}
|
|
105622
|
+
}
|
|
105623
|
+
return map3;
|
|
105624
|
+
}
|
|
105625
|
+
#handleFallbackAuthorizationRequest(message2) {
|
|
105626
|
+
const cohortId = message2.body?.cohortId;
|
|
105627
|
+
if (!cohortId) return;
|
|
105628
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105629
|
+
if (!state || !state.cohort) return;
|
|
105630
|
+
const acceptFrom = [
|
|
105631
|
+
"ValidationSent" /* ValidationSent */,
|
|
105632
|
+
"AwaitingSigning" /* AwaitingSigning */,
|
|
105633
|
+
"NonceSent" /* NonceSent */,
|
|
105634
|
+
"AwaitingPartialSig" /* AwaitingPartialSig */,
|
|
105635
|
+
"Complete" /* Complete */
|
|
105636
|
+
];
|
|
105637
|
+
if (!acceptFrom.includes(state.phase)) return;
|
|
105638
|
+
const sessionId = message2.body?.sessionId;
|
|
105639
|
+
const pendingTxHex = message2.body?.pendingTx;
|
|
105640
|
+
const prevOutScriptHex = message2.body?.prevOutScriptHex;
|
|
105641
|
+
const prevOutValue = message2.body?.prevOutValue;
|
|
105642
|
+
const fallbackLeafScriptHex = message2.body?.fallbackLeafScriptHex;
|
|
105643
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
|
|
105644
|
+
state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
|
|
105645
|
+
state.signingSession?.clearSecrets();
|
|
105646
|
+
state.phase = "AwaitingFallbackSig" /* AwaitingFallbackSig */;
|
|
105647
|
+
}
|
|
105648
|
+
/**
|
|
105649
|
+
* User action: authorize the fallback spend. Recomputes the k-of-n fallback
|
|
105650
|
+
* leaf from the member's OWN cohort state (not the service-provided script),
|
|
105651
|
+
* computes the BIP-341 script-path sighash over the requested transaction, and
|
|
105652
|
+
* returns a standalone BIP-340 signature (no nonce round). The member completes
|
|
105653
|
+
* once it has contributed; the service needs only k of these.
|
|
105654
|
+
*/
|
|
105655
|
+
approveFallback(cohortId) {
|
|
105656
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105657
|
+
if (!state || state.phase !== "AwaitingFallbackSig" /* AwaitingFallbackSig */) {
|
|
105658
|
+
throw new AggregationParticipantError(
|
|
105659
|
+
`Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
|
|
105660
|
+
"INVALID_PHASE",
|
|
105661
|
+
{ cohortId, phase: state?.phase }
|
|
105662
|
+
);
|
|
105663
|
+
}
|
|
105664
|
+
if (!state.fallbackRequest || !state.cohort) {
|
|
105665
|
+
throw new AggregationParticipantError(
|
|
105666
|
+
`Cohort ${cohortId} missing fallback request or cohort state.`,
|
|
105667
|
+
"MISSING_STATE",
|
|
105668
|
+
{ cohortId }
|
|
105669
|
+
);
|
|
105670
|
+
}
|
|
105671
|
+
const req = state.fallbackRequest;
|
|
105672
|
+
const tx = Transaction.fromRaw(hexToBytes(req.pendingTxHex), { allowUnknownOutputs: true });
|
|
105673
|
+
const prevOutScript = hexToBytes(req.prevOutScriptHex);
|
|
105674
|
+
const prevOutValue = BigInt(req.prevOutValue);
|
|
105675
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
105676
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
105677
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105678
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
105679
|
+
});
|
|
105680
|
+
const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
105681
|
+
const signature = this.#signer.withSecret((secretKey) => schnorr2.sign(sighash, secretKey));
|
|
105682
|
+
state.phase = "Complete" /* Complete */;
|
|
105683
|
+
return [createFallbackSignatureMessage({
|
|
105684
|
+
from: this.did,
|
|
105685
|
+
to: state.serviceDid,
|
|
105686
|
+
cohortId,
|
|
105687
|
+
sessionId: req.sessionId,
|
|
105688
|
+
signerPk: this.publicKey.slice(1),
|
|
105689
|
+
fallbackSignature: signature
|
|
105690
|
+
})];
|
|
105691
|
+
}
|
|
104927
105692
|
getCohortPhase(cohortId) {
|
|
104928
105693
|
return this.#cohortStates.get(cohortId)?.phase;
|
|
104929
105694
|
}
|
|
@@ -104960,6 +105725,85 @@ a=end-of-candidates
|
|
|
104960
105725
|
}
|
|
104961
105726
|
};
|
|
104962
105727
|
|
|
105728
|
+
// src/core/aggregation/recovery-spend.ts
|
|
105729
|
+
init_shim();
|
|
105730
|
+
init_secp256k1();
|
|
105731
|
+
var DUST_LIMIT_SATS = 546n;
|
|
105732
|
+
function bytesEqual2(a2, b) {
|
|
105733
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
105734
|
+
}
|
|
105735
|
+
function buildRecoverySpend(params) {
|
|
105736
|
+
const {
|
|
105737
|
+
cohortKeys,
|
|
105738
|
+
recoverySecretKey,
|
|
105739
|
+
recoveryKey,
|
|
105740
|
+
recoverySequence,
|
|
105741
|
+
fallbackThreshold,
|
|
105742
|
+
fundingModel,
|
|
105743
|
+
network,
|
|
105744
|
+
utxo,
|
|
105745
|
+
destinationAddress,
|
|
105746
|
+
fee,
|
|
105747
|
+
beaconAddress
|
|
105748
|
+
} = params;
|
|
105749
|
+
if (cohortKeys.length === 0) {
|
|
105750
|
+
throw new AggregationCohortError(
|
|
105751
|
+
"Cannot build recovery spend: no cohort keys.",
|
|
105752
|
+
"NO_COHORT_KEYS"
|
|
105753
|
+
);
|
|
105754
|
+
}
|
|
105755
|
+
const derivedPub = schnorr2.getPublicKey(recoverySecretKey);
|
|
105756
|
+
if (!bytesEqual2(derivedPub, recoveryKey)) {
|
|
105757
|
+
throw new AggregationCohortError(
|
|
105758
|
+
"Recovery secret key does not correspond to the committed recovery key.",
|
|
105759
|
+
"RECOVERY_KEY_MISMATCH"
|
|
105760
|
+
);
|
|
105761
|
+
}
|
|
105762
|
+
const out = utxo.value - fee;
|
|
105763
|
+
if (out <= 0n) {
|
|
105764
|
+
throw new AggregationCohortError(
|
|
105765
|
+
`Recovery fee ${fee} exceeds UTXO value ${utxo.value}.`,
|
|
105766
|
+
"FEE_EXCEEDS_VALUE",
|
|
105767
|
+
{ fee: fee.toString(), value: utxo.value.toString() }
|
|
105768
|
+
);
|
|
105769
|
+
}
|
|
105770
|
+
if (out < DUST_LIMIT_SATS) {
|
|
105771
|
+
throw new AggregationCohortError(
|
|
105772
|
+
`Recovered output ${out} is below the dust limit ${DUST_LIMIT_SATS}; the spend would not relay.`,
|
|
105773
|
+
"DUST_OUTPUT",
|
|
105774
|
+
{ output: out.toString(), dustLimit: DUST_LIMIT_SATS.toString() }
|
|
105775
|
+
);
|
|
105776
|
+
}
|
|
105777
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
105778
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
105779
|
+
recoveryKey,
|
|
105780
|
+
recoverySequence,
|
|
105781
|
+
cohortKeys,
|
|
105782
|
+
fallbackThreshold: resolveFallbackThreshold(fallbackThreshold, cohortKeys.length)
|
|
105783
|
+
});
|
|
105784
|
+
const net = getNetwork(network);
|
|
105785
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
105786
|
+
if (beaconAddress !== void 0 && payment.address !== beaconAddress) {
|
|
105787
|
+
throw new AggregationCohortError(
|
|
105788
|
+
`Reconstructed beacon address ${payment.address} does not match the cohort's funded address ${beaconAddress}.`,
|
|
105789
|
+
"BEACON_ADDRESS_MISMATCH",
|
|
105790
|
+
{ reconstructed: payment.address, expected: beaconAddress }
|
|
105791
|
+
);
|
|
105792
|
+
}
|
|
105793
|
+
const tx = new Transaction({ version: 2, allowUnknownInputs: true });
|
|
105794
|
+
tx.addInput({
|
|
105795
|
+
txid: utxo.txid,
|
|
105796
|
+
index: utxo.vout,
|
|
105797
|
+
witnessUtxo: { script: payment.script, amount: utxo.value },
|
|
105798
|
+
tapLeafScript: payment.tapLeafScript,
|
|
105799
|
+
sequence: recoverySequence
|
|
105800
|
+
});
|
|
105801
|
+
tx.addOutputAddress(destinationAddress, out, net);
|
|
105802
|
+
tx.signIdx(recoverySecretKey, 0);
|
|
105803
|
+
tx.finalize();
|
|
105804
|
+
return tx;
|
|
105805
|
+
}
|
|
105806
|
+
|
|
104963
105807
|
// src/core/aggregation/logger.ts
|
|
104964
105808
|
init_shim();
|
|
104965
105809
|
var CONSOLE_LOGGER = {
|
|
@@ -104992,6 +105836,7 @@ a=end-of-candidates
|
|
|
104992
105836
|
]);
|
|
104993
105837
|
var UPDATE_VALUES = /* @__PURE__ */ new Set([
|
|
104994
105838
|
SUBMIT_UPDATE,
|
|
105839
|
+
SUBMIT_NONINCLUDED,
|
|
104995
105840
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
104996
105841
|
VALIDATION_ACK
|
|
104997
105842
|
]);
|
|
@@ -104999,7 +105844,9 @@ a=end-of-candidates
|
|
|
104999
105844
|
AUTHORIZATION_REQUEST,
|
|
105000
105845
|
NONCE_CONTRIBUTION,
|
|
105001
105846
|
AGGREGATED_NONCE,
|
|
105002
|
-
SIGNATURE_AUTHORIZATION
|
|
105847
|
+
SIGNATURE_AUTHORIZATION,
|
|
105848
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
105849
|
+
FALLBACK_SIGNATURE
|
|
105003
105850
|
]);
|
|
105004
105851
|
function isAggregationMessageType(type) {
|
|
105005
105852
|
return KEYGEN_VALUES.has(type) || UPDATE_VALUES.has(type) || SIGN_VALUES.has(type);
|
|
@@ -111160,7 +112007,7 @@ ${value2}`;
|
|
|
111160
112007
|
* backfill historical events to late subscribers: republishing gives late
|
|
111161
112008
|
* joiners a window to discover the message without requiring protocol
|
|
111162
112009
|
* changes. Relay rate-limit / publish failures inside the interval are
|
|
111163
|
-
* caught and logged rather than propagated
|
|
112010
|
+
* caught and logged rather than propagated - the caller should stop the
|
|
111164
112011
|
* repeater once the protocol condition is satisfied.
|
|
111165
112012
|
*/
|
|
111166
112013
|
publishRepeating(message2, sender, intervalMs, recipient) {
|
|
@@ -111621,6 +112468,13 @@ ${value2}`;
|
|
|
111621
112468
|
#cohortTtlMs;
|
|
111622
112469
|
#phaseTimeoutMs;
|
|
111623
112470
|
#advertRepeatIntervalMs;
|
|
112471
|
+
#autoFallbackOnStall;
|
|
112472
|
+
/** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
|
|
112473
|
+
static #SIGNING_PHASES = [
|
|
112474
|
+
"SigningStarted" /* SigningStarted */,
|
|
112475
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
112476
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
112477
|
+
];
|
|
111624
112478
|
/** Per-cohort run state, keyed by cohortId. */
|
|
111625
112479
|
#contexts = /* @__PURE__ */ new Map();
|
|
111626
112480
|
#handlersRegistered = false;
|
|
@@ -111638,6 +112492,7 @@ ${value2}`;
|
|
|
111638
112492
|
this.#cohortTtlMs = options2.cohortTtlMs;
|
|
111639
112493
|
this.#phaseTimeoutMs = options2.phaseTimeoutMs;
|
|
111640
112494
|
this.#advertRepeatIntervalMs = options2.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
112495
|
+
this.#autoFallbackOnStall = options2.autoFallbackOnStall ?? false;
|
|
111641
112496
|
this.session = new AggregationService({
|
|
111642
112497
|
// The coordinator never signs, so the state machine receives only the
|
|
111643
112498
|
// public half of the operator's keypair (see ADR 038). The full keypair
|
|
@@ -111799,11 +112654,42 @@ ${value2}`;
|
|
|
111799
112654
|
ctx.phaseTimer = void 0;
|
|
111800
112655
|
if (this.#phaseTimeoutMs === void 0) return;
|
|
111801
112656
|
ctx.phaseTimer = setTimeout(() => {
|
|
112657
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
112658
|
+
const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
|
|
112659
|
+
if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
|
|
112660
|
+
this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
|
|
112661
|
+
return;
|
|
112662
|
+
}
|
|
111802
112663
|
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
111803
112664
|
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
111804
112665
|
this.#failCohort(ctx, new Error(reason));
|
|
111805
112666
|
}, this.#phaseTimeoutMs);
|
|
111806
112667
|
}
|
|
112668
|
+
/**
|
|
112669
|
+
* Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
|
|
112670
|
+
* fallback (script-path) signatures instead (ADR 042). Idempotent and safe
|
|
112671
|
+
* against the optimistic completion: it commits the cohort to the fallback
|
|
112672
|
+
* path synchronously (the `committedPath` latch) before sending anything, so a
|
|
112673
|
+
* late optimistic signature can no longer complete-and-broadcast a competing
|
|
112674
|
+
* spend of the same UTXO. No-op if the cohort is unknown, already settled, or
|
|
112675
|
+
* already committed to a path.
|
|
112676
|
+
*
|
|
112677
|
+
* Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
|
|
112678
|
+
* set; otherwise call it from an operator decision (a UI "fall back now"
|
|
112679
|
+
* action). Throws only if the underlying state machine rejects the transition
|
|
112680
|
+
* (e.g. signing has not started).
|
|
112681
|
+
*/
|
|
112682
|
+
async triggerFallback(cohortId) {
|
|
112683
|
+
const ctx = this.#contexts.get(cohortId);
|
|
112684
|
+
if (!ctx || ctx.settled || ctx.committedPath) return;
|
|
112685
|
+
const messages2 = this.session.startFallbackSigning(cohortId);
|
|
112686
|
+
ctx.committedPath = "fallback";
|
|
112687
|
+
this.#stopAdvertRepeating(ctx);
|
|
112688
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
112689
|
+
const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
|
|
112690
|
+
this.emit("fallback-started", { cohortId, sessionId });
|
|
112691
|
+
await this.#sendAll(messages2);
|
|
112692
|
+
}
|
|
111807
112693
|
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
111808
112694
|
#onPhaseMaybeChanged(ctx) {
|
|
111809
112695
|
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
@@ -111892,13 +112778,15 @@ ${value2}`;
|
|
|
111892
112778
|
static #HANDLED_MESSAGE_TYPES = [
|
|
111893
112779
|
COHORT_OPT_IN,
|
|
111894
112780
|
SUBMIT_UPDATE,
|
|
112781
|
+
SUBMIT_NONINCLUDED,
|
|
111895
112782
|
VALIDATION_ACK,
|
|
111896
112783
|
NONCE_CONTRIBUTION,
|
|
111897
|
-
SIGNATURE_AUTHORIZATION
|
|
112784
|
+
SIGNATURE_AUTHORIZATION,
|
|
112785
|
+
FALLBACK_SIGNATURE
|
|
111898
112786
|
];
|
|
111899
112787
|
/**
|
|
111900
112788
|
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
111901
|
-
* are DID-scoped and cohort-agnostic
|
|
112789
|
+
* are DID-scoped and cohort-agnostic - one registration serves every cohort
|
|
111902
112790
|
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
111903
112791
|
* handler via the inbound message's cohortId.
|
|
111904
112792
|
*/
|
|
@@ -111907,9 +112795,11 @@ ${value2}`;
|
|
|
111907
112795
|
this.#handlersRegistered = true;
|
|
111908
112796
|
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
|
|
111909
112797
|
this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
|
|
112798
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
|
|
111910
112799
|
this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
|
|
111911
112800
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
111912
112801
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
112802
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
|
|
111913
112803
|
}
|
|
111914
112804
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
111915
112805
|
#unregisterHandlers() {
|
|
@@ -112070,6 +112960,32 @@ ${value2}`;
|
|
|
112070
112960
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
112071
112961
|
*/
|
|
112072
112962
|
async #handleSignatureAuthorization(msg) {
|
|
112963
|
+
if (this.#stopped) return;
|
|
112964
|
+
const ctx = this.#contextFor(msg);
|
|
112965
|
+
if (!ctx) return;
|
|
112966
|
+
try {
|
|
112967
|
+
this.session.receive(msg);
|
|
112968
|
+
this.#drainRejections(ctx);
|
|
112969
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
112970
|
+
if (ctx.committedPath === "fallback") return;
|
|
112971
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
112972
|
+
if (result) {
|
|
112973
|
+
ctx.committedPath = "optimistic";
|
|
112974
|
+
this.#completeCohort(ctx, result);
|
|
112975
|
+
}
|
|
112976
|
+
} catch (err) {
|
|
112977
|
+
this.#failCohort(ctx, err);
|
|
112978
|
+
}
|
|
112979
|
+
}
|
|
112980
|
+
/**
|
|
112981
|
+
* Handler for receiving fallback (k-of-n script-path) signatures. The state
|
|
112982
|
+
* machine assembles and finalizes the fallback spend once k valid signatures
|
|
112983
|
+
* are in; the result is then emitted and the cohort's completion resolves. The
|
|
112984
|
+
* cohort is already committed to the fallback path (via {@link triggerFallback}).
|
|
112985
|
+
* @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
|
|
112986
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
112987
|
+
*/
|
|
112988
|
+
async #handleFallbackSignature(msg) {
|
|
112073
112989
|
if (this.#stopped) return;
|
|
112074
112990
|
const ctx = this.#contextFor(msg);
|
|
112075
112991
|
if (!ctx) return;
|
|
@@ -112143,7 +113059,8 @@ ${value2}`;
|
|
|
112143
113059
|
COHORT_READY,
|
|
112144
113060
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
112145
113061
|
AUTHORIZATION_REQUEST,
|
|
112146
|
-
AGGREGATED_NONCE
|
|
113062
|
+
AGGREGATED_NONCE,
|
|
113063
|
+
FALLBACK_AUTHORIZATION_REQUEST
|
|
112147
113064
|
];
|
|
112148
113065
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
112149
113066
|
#unregisterHandlers() {
|
|
@@ -112178,7 +113095,7 @@ ${value2}`;
|
|
|
112178
113095
|
*
|
|
112179
113096
|
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
112180
113097
|
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
112181
|
-
* `start()`, and listen for `cohort-complete`
|
|
113098
|
+
* `start()`, and listen for `cohort-complete` - the runner already drives
|
|
112182
113099
|
* any number of cohorts concurrently.
|
|
112183
113100
|
*
|
|
112184
113101
|
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
@@ -112213,6 +113130,7 @@ ${value2}`;
|
|
|
112213
113130
|
this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
|
|
112214
113131
|
this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
|
|
112215
113132
|
this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
|
|
113133
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
|
|
112216
113134
|
}
|
|
112217
113135
|
/**
|
|
112218
113136
|
* Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
|
|
@@ -112268,8 +113186,13 @@ ${value2}`;
|
|
|
112268
113186
|
cohortId,
|
|
112269
113187
|
beaconAddress: info.beaconAddress
|
|
112270
113188
|
});
|
|
112271
|
-
|
|
112272
|
-
|
|
113189
|
+
if (signedUpdate === null) {
|
|
113190
|
+
await this.#sendAll(this.session.declineUpdate(cohortId));
|
|
113191
|
+
this.emit("update-declined", { cohortId });
|
|
113192
|
+
} else {
|
|
113193
|
+
await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
|
|
113194
|
+
this.emit("update-submitted", { cohortId });
|
|
113195
|
+
}
|
|
112273
113196
|
} catch (err) {
|
|
112274
113197
|
this.emit("error", err);
|
|
112275
113198
|
}
|
|
@@ -112345,23 +113268,62 @@ ${value2}`;
|
|
|
112345
113268
|
const cohortId = msg.body?.cohortId;
|
|
112346
113269
|
if (!cohortId) return;
|
|
112347
113270
|
await this.#sendAll(this.session.generatePartialSignature(cohortId));
|
|
112348
|
-
|
|
112349
|
-
|
|
112350
|
-
|
|
112351
|
-
|
|
112352
|
-
|
|
112353
|
-
|
|
112354
|
-
|
|
112355
|
-
|
|
112356
|
-
|
|
112357
|
-
|
|
112358
|
-
|
|
112359
|
-
|
|
113271
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
113272
|
+
} catch (err) {
|
|
113273
|
+
this.emit("error", err);
|
|
113274
|
+
}
|
|
113275
|
+
}
|
|
113276
|
+
/**
|
|
113277
|
+
* Internal: handler for fallback authorization requests. The service abandoned
|
|
113278
|
+
* the optimistic key path; the member authorizes the k-of-n script-path spend
|
|
113279
|
+
* of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
|
|
113280
|
+
* the decision, signs the fallback, and completes once it has contributed (the
|
|
113281
|
+
* service needs only k members).
|
|
113282
|
+
* @param {BaseMessage} msg - The received fallback authorization request message.
|
|
113283
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
113284
|
+
*/
|
|
113285
|
+
async #handleFallbackAuthorizationRequest(msg) {
|
|
113286
|
+
if (this.#stopped) return;
|
|
113287
|
+
try {
|
|
113288
|
+
this.session.receive(msg);
|
|
113289
|
+
const cohortId = msg.body?.cohortId;
|
|
113290
|
+
if (!cohortId) return;
|
|
113291
|
+
const req = this.session.pendingFallbackRequests.get(cohortId);
|
|
113292
|
+
if (!req) return;
|
|
113293
|
+
this.emit("fallback-requested", req);
|
|
113294
|
+
const decision = await this.#onApproveSigning(req);
|
|
113295
|
+
if (!decision.approved) {
|
|
113296
|
+
this.emit("cohort-failed", { cohortId, reason: "Fallback signing rejected by participant" });
|
|
113297
|
+
return;
|
|
112360
113298
|
}
|
|
113299
|
+
await this.#sendAll(this.session.approveFallback(cohortId));
|
|
113300
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
112361
113301
|
} catch (err) {
|
|
112362
113302
|
this.emit("error", err);
|
|
112363
113303
|
}
|
|
112364
113304
|
}
|
|
113305
|
+
/**
|
|
113306
|
+
* Internal: emit `cohort-complete` with the participant's retained sidecar once
|
|
113307
|
+
* the cohort has reached the Complete phase. Surfaces the CAS Announcement map
|
|
113308
|
+
* (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
|
|
113309
|
+
* DID resolution. Read via getValidation (not pendingValidations, which lists
|
|
113310
|
+
* only the AwaitingValidation phase) so the sidecar is still available at
|
|
113311
|
+
* Complete. Shared by the optimistic and fallback completion paths.
|
|
113312
|
+
*/
|
|
113313
|
+
#emitCohortCompleteIfDone(cohortId) {
|
|
113314
|
+
if (this.session.getCohortPhase(cohortId) !== "Complete" /* Complete */) return;
|
|
113315
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
113316
|
+
if (!info) return;
|
|
113317
|
+
const validation = this.session.getValidation(cohortId);
|
|
113318
|
+
this.emit("cohort-complete", {
|
|
113319
|
+
cohortId,
|
|
113320
|
+
beaconAddress: info.beaconAddress,
|
|
113321
|
+
beaconType: validation?.beaconType ?? "",
|
|
113322
|
+
included: validation?.included ?? true,
|
|
113323
|
+
casAnnouncement: validation?.casAnnouncement,
|
|
113324
|
+
smtProof: validation?.smtProof
|
|
113325
|
+
});
|
|
113326
|
+
}
|
|
112365
113327
|
/**
|
|
112366
113328
|
* Internal: send helper to ensure messages are sent sequentially. This is important for protocol
|
|
112367
113329
|
* correctness, as some transports may not guarantee message order if sent in parallel.
|
|
@@ -112378,6 +113340,7 @@ ${value2}`;
|
|
|
112378
113340
|
|
|
112379
113341
|
// src/core/aggregation/runner/aggregation-runner.ts
|
|
112380
113342
|
init_shim();
|
|
113343
|
+
init_utils();
|
|
112381
113344
|
var AggregationRunner = class {
|
|
112382
113345
|
/**
|
|
112383
113346
|
* Run a cohort of ONE participant entirely in-process and return the
|
|
@@ -112385,8 +113348,8 @@ ${value2}`;
|
|
|
112385
113348
|
*
|
|
112386
113349
|
* One party plays both the coordinating service and the lone participant,
|
|
112387
113350
|
* connected over an {@link InMemoryTransport} (no relay or HTTP server). This
|
|
112388
|
-
* makes the single-participant aggregate-beacon path
|
|
112389
|
-
* two-axis beacon matrix
|
|
113351
|
+
* makes the single-participant aggregate-beacon path (the N=1 corner of the
|
|
113352
|
+
* two-axis beacon matrix, see ADR 037) first-class, useful for generating
|
|
112390
113353
|
* and reproducing single-participant aggregate test vectors.
|
|
112391
113354
|
*
|
|
112392
113355
|
* The service advertises a cohort with `minParticipants: 1`; the participant
|
|
@@ -112404,11 +113367,13 @@ ${value2}`;
|
|
|
112404
113367
|
transport.registerPeer(options2.participant.did, options2.participant.keys.publicKey.compressed);
|
|
112405
113368
|
transport.registerPeer(options2.service.did, options2.service.keys.publicKey.compressed);
|
|
112406
113369
|
transport.start();
|
|
113370
|
+
const recoveryKey = options2.config.recoveryKey ?? bytesToHex(options2.service.keys.publicKey.compressed.slice(1));
|
|
113371
|
+
const recoverySequence = options2.config.recoverySequence ?? DEFAULT_RECOVERY_SEQUENCE;
|
|
112407
113372
|
const service = new AggregationServiceRunner({
|
|
112408
113373
|
transport,
|
|
112409
113374
|
did: options2.service.did,
|
|
112410
113375
|
keys: options2.service.keys,
|
|
112411
|
-
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType },
|
|
113376
|
+
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType, recoveryKey, recoverySequence, fundingModel: DEFAULT_FUNDING_MODEL },
|
|
112412
113377
|
onProvideTxData: options2.onProvideTxData,
|
|
112413
113378
|
cohortTtlMs: options2.cohortTtlMs,
|
|
112414
113379
|
phaseTimeoutMs: options2.phaseTimeoutMs,
|
|
@@ -112538,8 +113503,7 @@ ${value2}`;
|
|
|
112538
113503
|
async function buildAggregationBeaconTx(opts) {
|
|
112539
113504
|
const feeEstimator = opts.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
112540
113505
|
const { utxo, prevTxBytes } = await fetchSpendableUtxo(opts.beaconAddress, opts.bitcoin);
|
|
112541
|
-
const
|
|
112542
|
-
const witnessScript = tapOut.script;
|
|
113506
|
+
const witnessScript = OutScript.encode(Address(opts.network).decode(opts.beaconAddress));
|
|
112543
113507
|
const feeSats = await feeEstimator.estimateFee(P2TR_BEACON_TX_VSIZE);
|
|
112544
113508
|
if (BigInt(utxo.value) <= feeSats) {
|
|
112545
113509
|
throw new BeaconError(
|
|
@@ -112618,7 +113582,7 @@ ${value2}`;
|
|
|
112618
113582
|
*
|
|
112619
113583
|
* Composed from the three extracted phases ({@link buildSinglePartyTx},
|
|
112620
113584
|
* {@link signSinglePartyTx}, {@link broadcastRawTx}) so each piece can be exercised
|
|
112621
|
-
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead
|
|
113585
|
+
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead:
|
|
112622
113586
|
* the multi-party path can't share the signing phase, but the tx-construction
|
|
112623
113587
|
* plumbing (UTXO fetch + OP_RETURN output + change output) is shared.
|
|
112624
113588
|
*
|
|
@@ -112650,7 +113614,7 @@ ${value2}`;
|
|
|
112650
113614
|
*
|
|
112651
113615
|
* Detects the beacon address script kind (P2PKH / P2WPKH / P2TR) and configures
|
|
112652
113616
|
* the input accordingly. Validates that the signer's pubkey produces the beacon
|
|
112653
|
-
* address under that script kind
|
|
113617
|
+
* address under that script kind: without this check, a misconfigured caller
|
|
112654
113618
|
* would burn a real UTXO on a tx that fails at broadcast. Fees are computed from
|
|
112655
113619
|
* the per-kind {@link SINGLETON_BEACON_TX_VSIZE} constant, avoiding any probe-sign
|
|
112656
113620
|
* round-trip.
|
|
@@ -128537,7 +129501,7 @@ ${value2}`;
|
|
|
128537
129501
|
#verificationMethod;
|
|
128538
129502
|
#beaconService;
|
|
128539
129503
|
/**
|
|
128540
|
-
* @internal
|
|
129504
|
+
* @internal Use {@link DidBtcr2.update} to create instances.
|
|
128541
129505
|
*/
|
|
128542
129506
|
constructor(params) {
|
|
128543
129507
|
this.#sourceDocument = params.sourceDocument;
|
|
@@ -128574,7 +129538,7 @@ ${value2}`;
|
|
|
128574
129538
|
const targetDocument = JSONPatch.apply(sourceDocument, patches);
|
|
128575
129539
|
if (targetDocument.id !== sourceDocument.id) {
|
|
128576
129540
|
throw new UpdateError(
|
|
128577
|
-
`Patches must not change the DID document id (source "${sourceDocument.id}"
|
|
129541
|
+
`Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
|
|
128578
129542
|
INVALID_DID_UPDATE,
|
|
128579
129543
|
{ sourceId: sourceDocument.id, targetId: targetDocument.id }
|
|
128580
129544
|
);
|
|
@@ -128658,8 +129622,8 @@ ${value2}`;
|
|
|
128658
129622
|
}
|
|
128659
129623
|
/**
|
|
128660
129624
|
* Advance the state machine. Returns either:
|
|
128661
|
-
* - `{ status: 'action-required', needs }`
|
|
128662
|
-
* - `{ status: 'complete', result }`
|
|
129625
|
+
* - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
|
|
129626
|
+
* - `{ status: 'complete', result }` update is signed and broadcast
|
|
128663
129627
|
*/
|
|
128664
129628
|
advance() {
|
|
128665
129629
|
while (true) {
|
|
@@ -128672,7 +129636,7 @@ ${value2}`;
|
|
|
128672
129636
|
continue;
|
|
128673
129637
|
}
|
|
128674
129638
|
// Phase: Sign
|
|
128675
|
-
// Emit NeedSigningKey
|
|
129639
|
+
// Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
|
|
128676
129640
|
case "Sign": {
|
|
128677
129641
|
return {
|
|
128678
129642
|
status: "action-required",
|
|
@@ -128855,8 +129819,8 @@ ${value2}`;
|
|
|
128855
129819
|
*
|
|
128856
129820
|
* Factory method that validates the update parameters and returns a sans-I/O
|
|
128857
129821
|
* {@link Updater} state machine. The caller drives the updater through its
|
|
128858
|
-
* phases (Construct
|
|
128859
|
-
* `provide()`. The method package performs **zero I/O
|
|
129822
|
+
* phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
|
|
129823
|
+
* `provide()`. The method package performs **zero I/O**: signing key retrieval
|
|
128860
129824
|
* (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
|
|
128861
129825
|
*
|
|
128862
129826
|
* For a fully-wired version with Bitcoin broadcast and key handling, see
|
|
@@ -128974,7 +129938,7 @@ ${value2}`;
|
|
|
128974
129938
|
#unsortedUpdates = [];
|
|
128975
129939
|
#resolvedResponse = null;
|
|
128976
129940
|
/**
|
|
128977
|
-
* @internal
|
|
129941
|
+
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
128978
129942
|
*/
|
|
128979
129943
|
constructor(didComponents, sidecarData, currentDocument, options2) {
|
|
128980
129944
|
this.#didComponents = didComponents;
|
|
@@ -129210,8 +130174,8 @@ ${value2}`;
|
|
|
129210
130174
|
}
|
|
129211
130175
|
/**
|
|
129212
130176
|
* Advance the state machine. Returns either:
|
|
129213
|
-
* - `{ status: 'action-required', needs }`
|
|
129214
|
-
* - `{ status: 'resolved', result }`
|
|
130177
|
+
* - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
|
|
130178
|
+
* - `{ status: 'resolved', result }` - resolution complete
|
|
129215
130179
|
*
|
|
129216
130180
|
* Analogous to Rust's `Resolver::resolve()`.
|
|
129217
130181
|
*/
|