@did-btcr2/method 0.36.1 → 0.38.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 +1149 -126
- package/dist/browser.mjs +1149 -126
- package/dist/cjs/index.js +1244 -208
- 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 +12 -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 +104 -11
- 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 +116 -20
- 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/fee-estimator.js +8 -0
- package/dist/esm/core/beacon/fee-estimator.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 +19 -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 +47 -5
- 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 +66 -10
- 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/fee-estimator.d.ts +8 -0
- package/dist/types/core/beacon/fee-estimator.d.ts.map +1 -1
- 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 +34 -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 +144 -12
- 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 +149 -25
- package/src/core/beacon/cas-beacon.ts +7 -7
- package/src/core/beacon/fee-estimator.ts +9 -0
- 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
|
@@ -93172,6 +93172,7 @@ a=end-of-candidates
|
|
|
93172
93172
|
BeaconUtils: () => BeaconUtils,
|
|
93173
93173
|
CASBeacon: () => CASBeacon,
|
|
93174
93174
|
CASBeaconError: () => CASBeaconError,
|
|
93175
|
+
CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
|
|
93175
93176
|
COHORT_ADVERT: () => COHORT_ADVERT,
|
|
93176
93177
|
COHORT_OPT_IN: () => COHORT_OPT_IN,
|
|
93177
93178
|
COHORT_OPT_IN_ACCEPT: () => COHORT_OPT_IN_ACCEPT,
|
|
@@ -93180,17 +93181,23 @@ a=end-of-candidates
|
|
|
93180
93181
|
DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
93181
93182
|
DEFAULT_BROADCAST_LOOKBACK_MS: () => DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
93182
93183
|
DEFAULT_CLOCK_SKEW_SEC: () => DEFAULT_CLOCK_SKEW_SEC,
|
|
93184
|
+
DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
|
|
93185
|
+
DEFAULT_FUNDING_MODEL: () => DEFAULT_FUNDING_MODEL,
|
|
93183
93186
|
DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
93184
93187
|
DEFAULT_NONCE_LEN_BYTES: () => DEFAULT_NONCE_LEN_BYTES,
|
|
93185
93188
|
DEFAULT_NOSTR_RELAYS: () => DEFAULT_NOSTR_RELAYS,
|
|
93189
|
+
DEFAULT_RECOVERY_SEQUENCE: () => DEFAULT_RECOVERY_SEQUENCE,
|
|
93186
93190
|
DID_REGEX: () => DID_REGEX,
|
|
93187
93191
|
DISTRIBUTE_AGGREGATED_DATA: () => DISTRIBUTE_AGGREGATED_DATA,
|
|
93192
|
+
DUST_LIMIT_SATS: () => DUST_LIMIT_SATS2,
|
|
93188
93193
|
DidBtcr2: () => DidBtcr2,
|
|
93189
93194
|
DidCommTransport: () => DidCommTransport,
|
|
93190
93195
|
DidDocument: () => DidDocument,
|
|
93191
93196
|
DidDocumentBuilder: () => DidDocumentBuilder,
|
|
93192
93197
|
DidVerificationMethod: () => DidVerificationMethod,
|
|
93193
93198
|
Document: () => Document,
|
|
93199
|
+
FALLBACK_AUTHORIZATION_REQUEST: () => FALLBACK_AUTHORIZATION_REQUEST,
|
|
93200
|
+
FALLBACK_SIGNATURE: () => FALLBACK_SIGNATURE,
|
|
93194
93201
|
GenesisDocument: () => GenesisDocument,
|
|
93195
93202
|
HTTP_ENVELOPE_VERSION: () => HTTP_ENVELOPE_VERSION,
|
|
93196
93203
|
HTTP_ROUTE: () => HTTP_ROUTE,
|
|
@@ -93204,7 +93211,9 @@ a=end-of-candidates
|
|
|
93204
93211
|
InMemoryTransport: () => InMemoryTransport,
|
|
93205
93212
|
InboxBuffer: () => InboxBuffer,
|
|
93206
93213
|
KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
|
|
93214
|
+
KNOWN_FUNDING_MODELS: () => KNOWN_FUNDING_MODELS,
|
|
93207
93215
|
KeyPairAggregationSigner: () => KeyPairAggregationSigner,
|
|
93216
|
+
MAX_RECOVERY_SEQUENCE: () => MAX_RECOVERY_SEQUENCE,
|
|
93208
93217
|
NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
|
|
93209
93218
|
NonceCache: () => NonceCache,
|
|
93210
93219
|
NostrTransport: () => NostrTransport,
|
|
@@ -93221,6 +93230,7 @@ a=end-of-candidates
|
|
|
93221
93230
|
SMTBeacon: () => SMTBeacon,
|
|
93222
93231
|
SMTBeaconError: () => SMTBeaconError,
|
|
93223
93232
|
SSE_EVENT: () => SSE_EVENT,
|
|
93233
|
+
SUBMIT_NONINCLUDED: () => SUBMIT_NONINCLUDED,
|
|
93224
93234
|
SUBMIT_UPDATE: () => SUBMIT_UPDATE,
|
|
93225
93235
|
ServiceCohortPhase: () => ServiceCohortPhase,
|
|
93226
93236
|
SigningSessionError: () => SigningSessionError,
|
|
@@ -93229,13 +93239,20 @@ a=end-of-candidates
|
|
|
93229
93239
|
SingletonBeacon: () => SingletonBeacon,
|
|
93230
93240
|
SingletonBeaconError: () => SingletonBeaconError,
|
|
93231
93241
|
StaticFeeEstimator: () => StaticFeeEstimator,
|
|
93242
|
+
TAPROOT_LEAF_VERSION: () => TAPROOT_LEAF_VERSION,
|
|
93232
93243
|
TransportAdapterError: () => TransportAdapterError,
|
|
93233
93244
|
TransportError: () => TransportError,
|
|
93234
93245
|
TransportFactory: () => TransportFactory,
|
|
93235
93246
|
TypedEventEmitter: () => TypedEventEmitter,
|
|
93236
93247
|
Updater: () => Updater,
|
|
93237
93248
|
VALIDATION_ACK: () => VALIDATION_ACK,
|
|
93249
|
+
beaconTxVsize: () => beaconTxVsize,
|
|
93238
93250
|
buildAggregationBeaconTx: () => buildAggregationBeaconTx,
|
|
93251
|
+
buildFallbackLeaf: () => buildFallbackLeaf,
|
|
93252
|
+
buildFallbackSpend: () => buildFallbackSpend,
|
|
93253
|
+
buildRecoveryLeaves: () => buildRecoveryLeaves,
|
|
93254
|
+
buildRecoveryScript: () => buildRecoveryScript,
|
|
93255
|
+
buildRecoverySpend: () => buildRecoverySpend,
|
|
93239
93256
|
buildRequestAuth: () => buildRequestAuth,
|
|
93240
93257
|
createAggregatedNonceMessage: () => createAggregatedNonceMessage,
|
|
93241
93258
|
createAuthorizationRequestMessage: () => createAuthorizationRequestMessage,
|
|
@@ -93244,13 +93261,17 @@ a=end-of-candidates
|
|
|
93244
93261
|
createCohortOptInMessage: () => createCohortOptInMessage,
|
|
93245
93262
|
createCohortReadyMessage: () => createCohortReadyMessage,
|
|
93246
93263
|
createDistributeAggregatedDataMessage: () => createDistributeAggregatedDataMessage,
|
|
93264
|
+
createFallbackAuthorizationRequestMessage: () => createFallbackAuthorizationRequestMessage,
|
|
93265
|
+
createFallbackSignatureMessage: () => createFallbackSignatureMessage,
|
|
93247
93266
|
createNonceContributionMessage: () => createNonceContributionMessage,
|
|
93248
93267
|
createSignatureAuthorizationMessage: () => createSignatureAuthorizationMessage,
|
|
93268
|
+
createSubmitNonIncludedMessage: () => createSubmitNonIncludedMessage,
|
|
93249
93269
|
createSubmitUpdateMessage: () => createSubmitUpdateMessage,
|
|
93250
93270
|
createValidationAckMessage: () => createValidationAckMessage,
|
|
93251
93271
|
defaultReconnectBackoff: () => defaultReconnectBackoff,
|
|
93252
93272
|
deriveSingletonAddress: () => deriveSingletonAddress,
|
|
93253
93273
|
detectSingletonScriptKind: () => detectSingletonScriptKind,
|
|
93274
|
+
fallbackSighash: () => fallbackSighash,
|
|
93254
93275
|
formatSseComment: () => formatSseComment,
|
|
93255
93276
|
formatSseEvent: () => formatSseEvent,
|
|
93256
93277
|
getBeaconStrategy: () => getBeaconStrategy,
|
|
@@ -93262,10 +93283,13 @@ a=end-of-candidates
|
|
|
93262
93283
|
isCohortOptInMessage: () => isCohortOptInMessage,
|
|
93263
93284
|
isCohortReadyMessage: () => isCohortReadyMessage,
|
|
93264
93285
|
isDistributeAggregatedDataMessage: () => isDistributeAggregatedDataMessage,
|
|
93286
|
+
isFallbackAuthorizationRequestMessage: () => isFallbackAuthorizationRequestMessage,
|
|
93287
|
+
isFallbackSignatureMessage: () => isFallbackSignatureMessage,
|
|
93265
93288
|
isKeygenMessageType: () => isKeygenMessageType,
|
|
93266
93289
|
isNonceContributionMessage: () => isNonceContributionMessage,
|
|
93267
93290
|
isSignMessageType: () => isSignMessageType,
|
|
93268
93291
|
isSignatureAuthorizationMessage: () => isSignatureAuthorizationMessage,
|
|
93292
|
+
isSubmitNonIncludedMessage: () => isSubmitNonIncludedMessage,
|
|
93269
93293
|
isSubmitUpdateMessage: () => isSubmitUpdateMessage,
|
|
93270
93294
|
isUpdateMessageType: () => isUpdateMessageType,
|
|
93271
93295
|
isValidationAckMessage: () => isValidationAckMessage,
|
|
@@ -93274,8 +93298,11 @@ a=end-of-candidates
|
|
|
93274
93298
|
parseRequestAuth: () => parseRequestAuth,
|
|
93275
93299
|
parseSseStream: () => parseSseStream,
|
|
93276
93300
|
registerBeaconStrategy: () => registerBeaconStrategy,
|
|
93301
|
+
resolveChangeAddress: () => resolveChangeAddress,
|
|
93302
|
+
resolveFallbackThreshold: () => resolveFallbackThreshold,
|
|
93277
93303
|
reviveFromWire: () => reviveFromWire,
|
|
93278
93304
|
signEnvelope: () => signEnvelope,
|
|
93305
|
+
tapLeafHash: () => tapLeafHash2,
|
|
93279
93306
|
validateCohortConditions: () => validateCohortConditions,
|
|
93280
93307
|
verifyEnvelope: () => verifyEnvelope,
|
|
93281
93308
|
verifyRequestAuth: () => verifyRequestAuth
|
|
@@ -100261,7 +100288,7 @@ a=end-of-candidates
|
|
|
100261
100288
|
* @readonly
|
|
100262
100289
|
* Get signing ability of the Multikey: true if a local Secp256k1SecretKey
|
|
100263
100290
|
* is present (note: the SchnorrKeyPair.secretKey getter throws when absent;
|
|
100264
|
-
* that throw is the historical error contract
|
|
100291
|
+
* that throw is the historical error contract - see multikey tests) or if
|
|
100265
100292
|
* an external signer is available.
|
|
100266
100293
|
*/
|
|
100267
100294
|
get signer() {
|
|
@@ -100366,6 +100393,7 @@ a=end-of-candidates
|
|
|
100366
100393
|
init_shim();
|
|
100367
100394
|
|
|
100368
100395
|
// src/core/aggregation/service.ts
|
|
100396
|
+
init_secp256k1();
|
|
100369
100397
|
init_utils();
|
|
100370
100398
|
|
|
100371
100399
|
// src/core/aggregation/beacon-strategy.ts
|
|
@@ -100666,9 +100694,12 @@ a=end-of-candidates
|
|
|
100666
100694
|
getDistributePayload(cohort) {
|
|
100667
100695
|
return { casAnnouncement: cohort.casAnnouncement };
|
|
100668
100696
|
},
|
|
100669
|
-
validateParticipantView({ participantDid, expectedHash, body }) {
|
|
100697
|
+
validateParticipantView({ participantDid, included, expectedHash, body }) {
|
|
100670
100698
|
const casAnnouncement = body.casAnnouncement;
|
|
100671
100699
|
if (!casAnnouncement) return { matches: false };
|
|
100700
|
+
if (!included) {
|
|
100701
|
+
return { matches: !(participantDid in casAnnouncement), casAnnouncement };
|
|
100702
|
+
}
|
|
100672
100703
|
return {
|
|
100673
100704
|
matches: casAnnouncement[participantDid] === expectedHash,
|
|
100674
100705
|
casAnnouncement
|
|
@@ -100684,15 +100715,20 @@ a=end-of-candidates
|
|
|
100684
100715
|
const proof = cohort.smtProofs?.get(participantDid);
|
|
100685
100716
|
return { smtProof: proof };
|
|
100686
100717
|
},
|
|
100687
|
-
validateParticipantView({ participantDid, submittedUpdate, body }) {
|
|
100718
|
+
validateParticipantView({ participantDid, included, submittedUpdate, body }) {
|
|
100688
100719
|
const smtProof = body.smtProof;
|
|
100689
|
-
|
|
100720
|
+
const index = didToIndex(participantDid);
|
|
100721
|
+
if (!included) {
|
|
100722
|
+
if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
|
|
100723
|
+
const candidateHash2 = blockHash(blockHash(base64UrlToHash(smtProof.nonce)));
|
|
100724
|
+
return { matches: verifySerializedProof(smtProof, index, candidateHash2), smtProof };
|
|
100725
|
+
}
|
|
100726
|
+
if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
|
|
100690
100727
|
const canonicalBytes = new TextEncoder().encode(canonicalize2(submittedUpdate));
|
|
100691
100728
|
const expectedUpdateId = hashToBase64Url(blockHash(canonicalBytes));
|
|
100692
100729
|
if (smtProof.updateId !== expectedUpdateId) {
|
|
100693
100730
|
return { matches: false, smtProof };
|
|
100694
100731
|
}
|
|
100695
|
-
const index = didToIndex(participantDid);
|
|
100696
100732
|
const candidateHash = blockHash(blockHash(base64UrlToHash(smtProof.nonce)), base64UrlToHash(smtProof.updateId));
|
|
100697
100733
|
return {
|
|
100698
100734
|
matches: verifySerializedProof(smtProof, index, candidateHash),
|
|
@@ -101691,6 +101727,15 @@ a=end-of-candidates
|
|
|
101691
101727
|
checkWSH(r2, witnessScript);
|
|
101692
101728
|
}
|
|
101693
101729
|
}
|
|
101730
|
+
function uniqPubkey(pubkeys) {
|
|
101731
|
+
const map3 = {};
|
|
101732
|
+
for (const pub of pubkeys) {
|
|
101733
|
+
const key = hex.encode(pub);
|
|
101734
|
+
if (map3[key])
|
|
101735
|
+
throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
|
|
101736
|
+
map3[key] = true;
|
|
101737
|
+
}
|
|
101738
|
+
}
|
|
101694
101739
|
var p2pkh = (publicKey2, network = NETWORK) => {
|
|
101695
101740
|
if (!isValidPubkey(publicKey2, PubT.ecdsa))
|
|
101696
101741
|
throw new Error("P2PKH: invalid publicKey");
|
|
@@ -101863,6 +101908,14 @@ a=end-of-candidates
|
|
|
101863
101908
|
};
|
|
101864
101909
|
}
|
|
101865
101910
|
}
|
|
101911
|
+
function p2tr_ms(m2, pubkeys, allowSamePubkeys = false) {
|
|
101912
|
+
if (!allowSamePubkeys)
|
|
101913
|
+
uniqPubkey(pubkeys);
|
|
101914
|
+
return {
|
|
101915
|
+
type: "tr_ms",
|
|
101916
|
+
script: OutScript.encode({ type: "tr_ms", pubkeys, m: m2 })
|
|
101917
|
+
};
|
|
101918
|
+
}
|
|
101866
101919
|
var base58check = createBase58check(sha256);
|
|
101867
101920
|
function validateWitness(version3, data) {
|
|
101868
101921
|
if (data.length < 2 || data.length > 40)
|
|
@@ -103338,6 +103391,104 @@ a=end-of-candidates
|
|
|
103338
103391
|
}
|
|
103339
103392
|
};
|
|
103340
103393
|
|
|
103394
|
+
// src/core/aggregation/recovery-policy.ts
|
|
103395
|
+
init_shim();
|
|
103396
|
+
init_secp256k1();
|
|
103397
|
+
init_utils();
|
|
103398
|
+
var DEFAULT_FUNDING_MODEL = "operator-funded";
|
|
103399
|
+
var DEFAULT_RECOVERY_SEQUENCE = 144;
|
|
103400
|
+
var MAX_RECOVERY_SEQUENCE = 65535;
|
|
103401
|
+
var TAPROOT_LEAF_VERSION = 192;
|
|
103402
|
+
function compactSize(n2) {
|
|
103403
|
+
if (n2 < 253) return new Uint8Array([n2]);
|
|
103404
|
+
if (n2 <= 65535) return new Uint8Array([253, n2 & 255, n2 >> 8 & 255]);
|
|
103405
|
+
return new Uint8Array([254, n2 & 255, n2 >> 8 & 255, n2 >> 16 & 255, n2 >> 24 & 255]);
|
|
103406
|
+
}
|
|
103407
|
+
function tapLeafHash2(script, leafVersion = TAPROOT_LEAF_VERSION) {
|
|
103408
|
+
return schnorr2.utils.taggedHash(
|
|
103409
|
+
"TapLeaf",
|
|
103410
|
+
concatBytes(new Uint8Array([leafVersion]), compactSize(script.length), script)
|
|
103411
|
+
);
|
|
103412
|
+
}
|
|
103413
|
+
function resolveFallbackThreshold(advertised, n2) {
|
|
103414
|
+
return advertised ?? Math.max(1, n2 - 1);
|
|
103415
|
+
}
|
|
103416
|
+
function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
|
|
103417
|
+
const n2 = cohortKeys.length;
|
|
103418
|
+
if (n2 === 0) {
|
|
103419
|
+
throw new AggregationCohortError(
|
|
103420
|
+
"Cannot build fallback leaf: no cohort keys.",
|
|
103421
|
+
"NO_COHORT_KEYS"
|
|
103422
|
+
);
|
|
103423
|
+
}
|
|
103424
|
+
for (const key of cohortKeys) {
|
|
103425
|
+
if (key.length !== 33) {
|
|
103426
|
+
throw new AggregationCohortError(
|
|
103427
|
+
`Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
|
|
103428
|
+
"INVALID_COHORT_KEY",
|
|
103429
|
+
{ length: key.length }
|
|
103430
|
+
);
|
|
103431
|
+
}
|
|
103432
|
+
}
|
|
103433
|
+
if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n2) {
|
|
103434
|
+
throw new AggregationCohortError(
|
|
103435
|
+
`Fallback threshold must be an integer in [1, ${n2}] (k-of-n), got ${fallbackThreshold}.`,
|
|
103436
|
+
"INVALID_FALLBACK_THRESHOLD",
|
|
103437
|
+
{ fallbackThreshold, n: n2 }
|
|
103438
|
+
);
|
|
103439
|
+
}
|
|
103440
|
+
}
|
|
103441
|
+
function buildFallbackLeaf(params) {
|
|
103442
|
+
assertFallbackParams(params);
|
|
103443
|
+
const xOnlyKeys = sortKeys(params.cohortKeys).map((k) => k.slice(1));
|
|
103444
|
+
return p2tr_ms(params.fallbackThreshold, xOnlyKeys).script;
|
|
103445
|
+
}
|
|
103446
|
+
function assertRecoveryParams({ recoveryKey, recoverySequence }) {
|
|
103447
|
+
if (recoveryKey.length !== 32) {
|
|
103448
|
+
throw new AggregationCohortError(
|
|
103449
|
+
`Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
|
|
103450
|
+
"INVALID_RECOVERY_KEY"
|
|
103451
|
+
);
|
|
103452
|
+
}
|
|
103453
|
+
if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103454
|
+
throw new AggregationCohortError(
|
|
103455
|
+
`Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
|
|
103456
|
+
"INVALID_RECOVERY_SEQUENCE"
|
|
103457
|
+
);
|
|
103458
|
+
}
|
|
103459
|
+
}
|
|
103460
|
+
function buildRecoveryScript(params) {
|
|
103461
|
+
assertRecoveryParams(params);
|
|
103462
|
+
return Script.encode([
|
|
103463
|
+
params.recoverySequence,
|
|
103464
|
+
"CHECKSEQUENCEVERIFY",
|
|
103465
|
+
"DROP",
|
|
103466
|
+
params.recoveryKey,
|
|
103467
|
+
"CHECKSIG"
|
|
103468
|
+
]);
|
|
103469
|
+
}
|
|
103470
|
+
function buildRecoveryLeaves(fundingModel, params) {
|
|
103471
|
+
switch (fundingModel) {
|
|
103472
|
+
case "operator-funded":
|
|
103473
|
+
return [
|
|
103474
|
+
{ script: buildFallbackLeaf(params) },
|
|
103475
|
+
{ script: buildRecoveryScript(params) }
|
|
103476
|
+
];
|
|
103477
|
+
case "participant-funded":
|
|
103478
|
+
throw new AggregationCohortError(
|
|
103479
|
+
"Funding model 'participant-funded' is reserved and not yet implemented.",
|
|
103480
|
+
"UNSUPPORTED_FUNDING_MODEL",
|
|
103481
|
+
{ fundingModel }
|
|
103482
|
+
);
|
|
103483
|
+
default:
|
|
103484
|
+
throw new AggregationCohortError(
|
|
103485
|
+
`Unknown funding model: ${fundingModel}.`,
|
|
103486
|
+
"UNKNOWN_FUNDING_MODEL",
|
|
103487
|
+
{ fundingModel }
|
|
103488
|
+
);
|
|
103489
|
+
}
|
|
103490
|
+
}
|
|
103491
|
+
|
|
103341
103492
|
// src/core/aggregation/cohort.ts
|
|
103342
103493
|
var AggregationCohort = class {
|
|
103343
103494
|
/** Unique identifier for the cohort. */
|
|
@@ -103353,8 +103504,8 @@ a=end-of-candidates
|
|
|
103353
103504
|
/** List of participant DIDs that have been accepted into the cohort. */
|
|
103354
103505
|
participants = [];
|
|
103355
103506
|
/**
|
|
103356
|
-
* Mapping from participant DID
|
|
103357
|
-
* Distinct from {@link cohortKeys} (which is sorted per BIP-327)
|
|
103507
|
+
* Mapping from participant DID to their compressed secp256k1 public key.
|
|
103508
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
|
|
103358
103509
|
* callers look up a participant's key without knowing their position in the
|
|
103359
103510
|
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
103360
103511
|
*/
|
|
@@ -103362,16 +103513,41 @@ a=end-of-candidates
|
|
|
103362
103513
|
/** Sorted list of cohort participants' compressed public keys. */
|
|
103363
103514
|
#cohortKeys = [];
|
|
103364
103515
|
/**
|
|
103365
|
-
* BIP-341 TapTweak
|
|
103366
|
-
*
|
|
103367
|
-
*
|
|
103516
|
+
* BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
|
|
103517
|
+
* The beacon output is an internal key (the MuSig2 aggregate) plus a script
|
|
103518
|
+
* tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
|
|
103519
|
+
* The MuSig2 signing session applies this as an x-only tweak; a value that does
|
|
103520
|
+
* not match the root the funded address was derived from silently yields an
|
|
103521
|
+
* invalid key-path signature.
|
|
103368
103522
|
*/
|
|
103369
103523
|
tapTweak = new Uint8Array();
|
|
103370
|
-
/** The n-of-n MuSig2
|
|
103524
|
+
/** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
|
|
103525
|
+
internalKey = new Uint8Array();
|
|
103526
|
+
/** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
|
|
103527
|
+
tapMerkleRoot = new Uint8Array();
|
|
103528
|
+
/** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
|
|
103529
|
+
recoveryKey;
|
|
103530
|
+
/** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
|
|
103531
|
+
recoverySequence;
|
|
103532
|
+
/** Funding model governing the recovery leaves (default 'operator-funded'). */
|
|
103533
|
+
fundingModel;
|
|
103534
|
+
/**
|
|
103535
|
+
* Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
|
|
103536
|
+
* address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
|
|
103537
|
+
*/
|
|
103538
|
+
fallbackThreshold;
|
|
103539
|
+
/** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
|
|
103371
103540
|
beaconAddress = "";
|
|
103372
103541
|
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
103373
103542
|
pendingUpdates = /* @__PURE__ */ new Map();
|
|
103374
|
-
/**
|
|
103543
|
+
/**
|
|
103544
|
+
* Participant DIDs that explicitly declined to submit an update this round
|
|
103545
|
+
* (cooperative non-inclusion). A decliner is absent from the CAS Announcement
|
|
103546
|
+
* Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
|
|
103547
|
+
* disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
|
|
103548
|
+
*/
|
|
103549
|
+
nonIncluded = /* @__PURE__ */ new Set();
|
|
103550
|
+
/** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
|
|
103375
103551
|
casAnnouncement;
|
|
103376
103552
|
/** Per-participant SMT proofs, set by buildSMTTree(). */
|
|
103377
103553
|
smtProofs;
|
|
@@ -103381,12 +103557,16 @@ a=end-of-candidates
|
|
|
103381
103557
|
validationAcks = /* @__PURE__ */ new Set();
|
|
103382
103558
|
/** Set of participant DIDs that have rejected the aggregated data. */
|
|
103383
103559
|
validationRejections = /* @__PURE__ */ new Set();
|
|
103384
|
-
constructor({ id, minParticipants, serviceDid, network, beaconType }) {
|
|
103560
|
+
constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
|
|
103385
103561
|
this.id = id || crypto.randomUUID();
|
|
103386
103562
|
this.minParticipants = minParticipants ?? 2;
|
|
103387
103563
|
this.serviceDid = serviceDid || "";
|
|
103388
103564
|
this.network = network;
|
|
103389
103565
|
this.beaconType = beaconType || "CASBeacon";
|
|
103566
|
+
this.recoveryKey = recoveryKey;
|
|
103567
|
+
this.recoverySequence = recoverySequence;
|
|
103568
|
+
this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
|
|
103569
|
+
this.fallbackThreshold = fallbackThreshold;
|
|
103390
103570
|
}
|
|
103391
103571
|
/** Sorted cohort keys (sorted on assignment per BIP-327). */
|
|
103392
103572
|
get cohortKeys() {
|
|
@@ -103396,8 +103576,26 @@ a=end-of-candidates
|
|
|
103396
103576
|
this.#cohortKeys = sortKeys(keys);
|
|
103397
103577
|
}
|
|
103398
103578
|
/**
|
|
103399
|
-
*
|
|
103400
|
-
*
|
|
103579
|
+
* The resolved k of the k-of-n fallback leaf: the advertised
|
|
103580
|
+
* {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
|
|
103581
|
+
* the current cohort size. This is the value the beacon address commits to and
|
|
103582
|
+
* the spend builders must reproduce. Returns 0 before any cohort keys are set.
|
|
103583
|
+
*/
|
|
103584
|
+
get effectiveFallbackThreshold() {
|
|
103585
|
+
if (this.#cohortKeys.length === 0) return 0;
|
|
103586
|
+
return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
|
|
103587
|
+
}
|
|
103588
|
+
/**
|
|
103589
|
+
* Computes the Taproot beacon address from the cohort keys and recovery params.
|
|
103590
|
+
*
|
|
103591
|
+
* The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
|
|
103592
|
+
* script path is the recovery tree (a CSV recovery leaf so a missing signer
|
|
103593
|
+
* cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
|
|
103594
|
+
* the `tapTweak` the MuSig2 session must apply for the key-path spend.
|
|
103595
|
+
*
|
|
103596
|
+
* The tweak is derived from the Merkle root the address was built with (read
|
|
103597
|
+
* back from the payment), never recomputed by hand, so the MuSig2 key-path
|
|
103598
|
+
* signature is guaranteed to validate against the funded address.
|
|
103401
103599
|
*/
|
|
103402
103600
|
computeBeaconAddress() {
|
|
103403
103601
|
if (this.#cohortKeys.length === 0) {
|
|
@@ -103407,10 +103605,25 @@ a=end-of-candidates
|
|
|
103407
103605
|
{ cohortId: this.id }
|
|
103408
103606
|
);
|
|
103409
103607
|
}
|
|
103608
|
+
if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
|
|
103609
|
+
throw new AggregationCohortError(
|
|
103610
|
+
"Cannot compute beacon address: missing recovery key or sequence.",
|
|
103611
|
+
"NO_RECOVERY_PARAMS",
|
|
103612
|
+
{ cohortId: this.id }
|
|
103613
|
+
);
|
|
103614
|
+
}
|
|
103410
103615
|
const keyAggContext = keyAggregate(this.#cohortKeys);
|
|
103411
103616
|
const aggPubkey = keyAggExport(keyAggContext);
|
|
103412
|
-
const
|
|
103413
|
-
|
|
103617
|
+
const leaves = buildRecoveryLeaves(this.fundingModel, {
|
|
103618
|
+
recoveryKey: this.recoveryKey,
|
|
103619
|
+
recoverySequence: this.recoverySequence,
|
|
103620
|
+
cohortKeys: this.#cohortKeys,
|
|
103621
|
+
fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
|
|
103622
|
+
});
|
|
103623
|
+
const payment = p2tr(aggPubkey, leaves, getNetwork(this.network), true);
|
|
103624
|
+
this.internalKey = aggPubkey;
|
|
103625
|
+
this.tapMerkleRoot = payment.tapMerkleRoot;
|
|
103626
|
+
this.tapTweak = schnorr2.utils.taggedHash("TapTweak", concatBytes(aggPubkey, payment.tapMerkleRoot));
|
|
103414
103627
|
if (!payment.address) {
|
|
103415
103628
|
throw new AggregationCohortError(
|
|
103416
103629
|
"Failed to compute Taproot address",
|
|
@@ -103466,20 +103679,54 @@ a=end-of-candidates
|
|
|
103466
103679
|
}
|
|
103467
103680
|
this.pendingUpdates.set(participantDid, signedUpdate);
|
|
103468
103681
|
}
|
|
103682
|
+
/**
|
|
103683
|
+
* Record that a participant declined to submit an update this round
|
|
103684
|
+
* (cooperative non-inclusion). The member stays in the cohort and still signs.
|
|
103685
|
+
*/
|
|
103686
|
+
addNonInclusion(participantDid) {
|
|
103687
|
+
if (!this.participants.includes(participantDid)) {
|
|
103688
|
+
throw new AggregationCohortError(
|
|
103689
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
103690
|
+
"UNKNOWN_PARTICIPANT",
|
|
103691
|
+
{ cohortId: this.id, participantDid }
|
|
103692
|
+
);
|
|
103693
|
+
}
|
|
103694
|
+
if (this.pendingUpdates.has(participantDid)) {
|
|
103695
|
+
throw new AggregationCohortError(
|
|
103696
|
+
`Participant ${participantDid} already submitted an update; cannot also decline.`,
|
|
103697
|
+
"CONFLICTING_RESPONSE",
|
|
103698
|
+
{ cohortId: this.id, participantDid }
|
|
103699
|
+
);
|
|
103700
|
+
}
|
|
103701
|
+
this.nonIncluded.add(participantDid);
|
|
103702
|
+
}
|
|
103469
103703
|
hasAllUpdates() {
|
|
103470
103704
|
return this.pendingUpdates.size === this.participants.length;
|
|
103471
103705
|
}
|
|
103706
|
+
/**
|
|
103707
|
+
* True when every participant has responded for this round, either with an
|
|
103708
|
+
* update or with an explicit non-inclusion. This is the aggregation gate when
|
|
103709
|
+
* non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
|
|
103710
|
+
* {@link hasAllValidationResponses} generalizes a unanimous ack.
|
|
103711
|
+
*/
|
|
103712
|
+
hasAllResponses() {
|
|
103713
|
+
return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
|
|
103714
|
+
}
|
|
103472
103715
|
/**
|
|
103473
103716
|
* Builds a CAS Announcement Map from collected updates.
|
|
103474
|
-
* Maps each participant DID
|
|
103717
|
+
* Maps each participant DID to base64url canonical hash of their signed update.
|
|
103475
103718
|
* Computes signal bytes as SHA-256 of canonicalized announcement.
|
|
103719
|
+
*
|
|
103720
|
+
* Members who declined (cooperative non-inclusion) are naturally absent from
|
|
103721
|
+
* the map: the body iterates {@link pendingUpdates}, which never holds a
|
|
103722
|
+
* decliner. Absence from the map is exactly the CAS non-inclusion signal.
|
|
103476
103723
|
*/
|
|
103477
103724
|
buildCASAnnouncement() {
|
|
103478
|
-
if (!this.
|
|
103725
|
+
if (!this.hasAllResponses()) {
|
|
103479
103726
|
throw new AggregationCohortError(
|
|
103480
|
-
"Cannot build CAS Announcement: not all
|
|
103481
|
-
"
|
|
103482
|
-
{ cohortId: this.id,
|
|
103727
|
+
"Cannot build CAS Announcement: not all participants have responded.",
|
|
103728
|
+
"INCOMPLETE_RESPONSES",
|
|
103729
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103483
103730
|
);
|
|
103484
103731
|
}
|
|
103485
103732
|
const announcement = {};
|
|
@@ -103491,31 +103738,41 @@ a=end-of-candidates
|
|
|
103491
103738
|
return announcement;
|
|
103492
103739
|
}
|
|
103493
103740
|
/**
|
|
103494
|
-
* Builds an SMT tree
|
|
103495
|
-
*
|
|
103496
|
-
*
|
|
103741
|
+
* Builds an SMT tree with one leaf per participant.
|
|
103742
|
+
*
|
|
103743
|
+
* A member who submitted an update gets an inclusion leaf
|
|
103744
|
+
* (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
|
|
103745
|
+
* non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
|
|
103746
|
+
* omitted). The cohort mints each member's nonce and returns it inside that
|
|
103747
|
+
* member's serialized proof, so a decliner can self-validate its own
|
|
103748
|
+
* non-inclusion slot and the resolver can recompute the leaf. Stores
|
|
103749
|
+
* per-participant proofs and the SMT root as signalBytes.
|
|
103497
103750
|
*/
|
|
103498
103751
|
buildSMTTree() {
|
|
103499
|
-
if (!this.
|
|
103752
|
+
if (!this.hasAllResponses()) {
|
|
103500
103753
|
throw new AggregationCohortError(
|
|
103501
|
-
"Cannot build SMT tree: not all
|
|
103502
|
-
"
|
|
103503
|
-
{ cohortId: this.id }
|
|
103754
|
+
"Cannot build SMT tree: not all participants have responded.",
|
|
103755
|
+
"INCOMPLETE_RESPONSES",
|
|
103756
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103504
103757
|
);
|
|
103505
103758
|
}
|
|
103506
103759
|
const tree2 = new BTCR2MerkleTree();
|
|
103507
103760
|
const entries = [];
|
|
103508
103761
|
const encoder4 = new TextEncoder();
|
|
103509
|
-
for (const
|
|
103510
|
-
const canonicalBytes = encoder4.encode(canonicalize2(signedUpdate));
|
|
103762
|
+
for (const did of this.participants) {
|
|
103511
103763
|
const nonce = randomBytes(32);
|
|
103512
|
-
|
|
103764
|
+
const signedUpdate = this.pendingUpdates.get(did);
|
|
103765
|
+
if (signedUpdate) {
|
|
103766
|
+
entries.push({ did, nonce, signedUpdate: encoder4.encode(canonicalize2(signedUpdate)) });
|
|
103767
|
+
} else {
|
|
103768
|
+
entries.push({ did, nonce });
|
|
103769
|
+
}
|
|
103513
103770
|
}
|
|
103514
103771
|
tree2.addEntries(entries);
|
|
103515
103772
|
tree2.finalize();
|
|
103516
103773
|
this.signalBytes = tree2.rootHash;
|
|
103517
103774
|
this.smtProofs = /* @__PURE__ */ new Map();
|
|
103518
|
-
for (const
|
|
103775
|
+
for (const did of this.participants) {
|
|
103519
103776
|
this.smtProofs.set(did, tree2.proof(did));
|
|
103520
103777
|
}
|
|
103521
103778
|
return this.smtProofs;
|
|
@@ -103541,7 +103798,7 @@ a=end-of-candidates
|
|
|
103541
103798
|
return this.validationAcks.size + this.validationRejections.size === this.participants.length;
|
|
103542
103799
|
}
|
|
103543
103800
|
/**
|
|
103544
|
-
* True when all participants approved. Note: differs from {@link hasAllValidationResponses}
|
|
103801
|
+
* True when all participants approved. Note: differs from {@link hasAllValidationResponses},
|
|
103545
103802
|
* this returns false if any participant rejected, even if all responses are in.
|
|
103546
103803
|
*/
|
|
103547
103804
|
isFullyValidated() {
|
|
@@ -103552,6 +103809,7 @@ a=end-of-candidates
|
|
|
103552
103809
|
// src/core/aggregation/conditions.ts
|
|
103553
103810
|
init_shim();
|
|
103554
103811
|
var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
|
|
103812
|
+
var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
|
|
103555
103813
|
function checkPair(problems, label, min, max) {
|
|
103556
103814
|
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
103557
103815
|
problems.push(`min${label} must be an integer >= 0`);
|
|
@@ -103595,11 +103853,128 @@ a=end-of-candidates
|
|
|
103595
103853
|
if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
|
|
103596
103854
|
problems.push("pendingUpdateTrigger must be an integer >= 1");
|
|
103597
103855
|
}
|
|
103856
|
+
if (c2.fallbackThreshold !== void 0) {
|
|
103857
|
+
if (!Number.isInteger(c2.fallbackThreshold) || c2.fallbackThreshold < 1) {
|
|
103858
|
+
problems.push("fallbackThreshold must be an integer >= 1");
|
|
103859
|
+
} else if (c2.maxParticipants !== void 0 && Number.isInteger(c2.maxParticipants) && c2.fallbackThreshold > c2.maxParticipants) {
|
|
103860
|
+
problems.push("fallbackThreshold must be <= maxParticipants");
|
|
103861
|
+
}
|
|
103862
|
+
}
|
|
103598
103863
|
checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
|
|
103599
103864
|
checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
|
|
103865
|
+
if (typeof c2.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c2.recoveryKey)) {
|
|
103866
|
+
problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
|
|
103867
|
+
}
|
|
103868
|
+
if (!Number.isInteger(c2.recoverySequence) || c2.recoverySequence < 1 || c2.recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103869
|
+
problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
|
|
103870
|
+
}
|
|
103871
|
+
if (c2.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c2.fundingModel)) {
|
|
103872
|
+
problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
|
|
103873
|
+
}
|
|
103600
103874
|
return problems;
|
|
103601
103875
|
}
|
|
103602
103876
|
|
|
103877
|
+
// src/core/aggregation/fallback-spend.ts
|
|
103878
|
+
init_shim();
|
|
103879
|
+
init_secp256k1();
|
|
103880
|
+
function bytesEqual(a2, b) {
|
|
103881
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
103882
|
+
}
|
|
103883
|
+
function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
|
|
103884
|
+
return tx.preimageWitnessV1(
|
|
103885
|
+
inputIndex,
|
|
103886
|
+
[prevOutScript],
|
|
103887
|
+
SigHash.DEFAULT,
|
|
103888
|
+
[prevOutValue],
|
|
103889
|
+
void 0,
|
|
103890
|
+
fallbackLeafScript,
|
|
103891
|
+
TAPROOT_LEAF_VERSION
|
|
103892
|
+
);
|
|
103893
|
+
}
|
|
103894
|
+
function buildFallbackSpend(params) {
|
|
103895
|
+
const {
|
|
103896
|
+
pendingTx,
|
|
103897
|
+
cohortKeys,
|
|
103898
|
+
recoveryKey,
|
|
103899
|
+
recoverySequence,
|
|
103900
|
+
fundingModel,
|
|
103901
|
+
network,
|
|
103902
|
+
prevOutScript,
|
|
103903
|
+
prevOutValue,
|
|
103904
|
+
signatures
|
|
103905
|
+
} = params;
|
|
103906
|
+
const inputIndex = params.inputIndex ?? 0;
|
|
103907
|
+
if (cohortKeys.length === 0) {
|
|
103908
|
+
throw new AggregationCohortError(
|
|
103909
|
+
"Cannot build fallback spend: no cohort keys.",
|
|
103910
|
+
"NO_COHORT_KEYS"
|
|
103911
|
+
);
|
|
103912
|
+
}
|
|
103913
|
+
const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
|
|
103914
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
103915
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
103916
|
+
recoveryKey,
|
|
103917
|
+
recoverySequence,
|
|
103918
|
+
cohortKeys,
|
|
103919
|
+
fallbackThreshold
|
|
103920
|
+
});
|
|
103921
|
+
const net = getNetwork(network);
|
|
103922
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
103923
|
+
if (!bytesEqual(payment.script, prevOutScript)) {
|
|
103924
|
+
throw new AggregationCohortError(
|
|
103925
|
+
"Reconstructed beacon output script does not match the spent prevout script.",
|
|
103926
|
+
"PREVOUT_SCRIPT_MISMATCH"
|
|
103927
|
+
);
|
|
103928
|
+
}
|
|
103929
|
+
const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
|
|
103930
|
+
const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
|
|
103931
|
+
const script = scriptVer.slice(0, scriptVer.length - 1);
|
|
103932
|
+
return bytesEqual(script, fallbackLeaf);
|
|
103933
|
+
});
|
|
103934
|
+
if (!leafEntry) {
|
|
103935
|
+
throw new AggregationCohortError(
|
|
103936
|
+
"Could not locate the fallback leaf in the reconstructed beacon output.",
|
|
103937
|
+
"FALLBACK_LEAF_NOT_FOUND"
|
|
103938
|
+
);
|
|
103939
|
+
}
|
|
103940
|
+
const cohortXOnly = sortKeys(cohortKeys).map((k) => k.slice(1));
|
|
103941
|
+
const tx = pendingTx;
|
|
103942
|
+
const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
|
|
103943
|
+
const leafHash = tapLeafHash2(fallbackLeaf);
|
|
103944
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
103945
|
+
for (const { pubKey, signature } of signatures) {
|
|
103946
|
+
if (pubKey.length !== 32 || signature.length !== 64) continue;
|
|
103947
|
+
const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
|
|
103948
|
+
if (!isCohortKey) continue;
|
|
103949
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103950
|
+
if (accepted.has(hex2)) continue;
|
|
103951
|
+
let ok = false;
|
|
103952
|
+
try {
|
|
103953
|
+
ok = schnorr2.verify(signature, sighash, pubKey);
|
|
103954
|
+
} catch {
|
|
103955
|
+
ok = false;
|
|
103956
|
+
}
|
|
103957
|
+
if (ok) accepted.set(hex2, signature);
|
|
103958
|
+
}
|
|
103959
|
+
if (accepted.size < fallbackThreshold) {
|
|
103960
|
+
throw new AggregationCohortError(
|
|
103961
|
+
`Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
|
|
103962
|
+
"NOT_ENOUGH_FALLBACK_SIGNATURES",
|
|
103963
|
+
{ have: accepted.size, need: fallbackThreshold }
|
|
103964
|
+
);
|
|
103965
|
+
}
|
|
103966
|
+
const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
|
|
103967
|
+
tx.updateInput(inputIndex, {
|
|
103968
|
+
tapLeafScript: [leafEntry],
|
|
103969
|
+
tapScriptSig: chosen.map((pubKey) => {
|
|
103970
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103971
|
+
return [{ pubKey, leafHash }, accepted.get(hex2)];
|
|
103972
|
+
})
|
|
103973
|
+
});
|
|
103974
|
+
tx.finalize();
|
|
103975
|
+
return tx;
|
|
103976
|
+
}
|
|
103977
|
+
|
|
103603
103978
|
// src/core/aggregation/messages/base.ts
|
|
103604
103979
|
init_shim();
|
|
103605
103980
|
var AGGREGATION_WIRE_VERSION = 1;
|
|
@@ -103639,12 +104014,15 @@ a=end-of-candidates
|
|
|
103639
104014
|
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
103640
104015
|
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
103641
104016
|
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
104017
|
+
var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
|
|
103642
104018
|
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
103643
104019
|
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
103644
104020
|
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
103645
104021
|
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
103646
104022
|
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
103647
104023
|
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
104024
|
+
var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
|
|
104025
|
+
var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
|
|
103648
104026
|
|
|
103649
104027
|
// src/core/aggregation/messages/factories.ts
|
|
103650
104028
|
init_shim();
|
|
@@ -103668,6 +104046,10 @@ a=end-of-candidates
|
|
|
103668
104046
|
const { from: from8, to, ...body } = fields;
|
|
103669
104047
|
return new BaseMessage({ type: SUBMIT_UPDATE, from: from8, to, body });
|
|
103670
104048
|
}
|
|
104049
|
+
function createSubmitNonIncludedMessage(fields) {
|
|
104050
|
+
const { from: from8, to, ...body } = fields;
|
|
104051
|
+
return new BaseMessage({ type: SUBMIT_NONINCLUDED, from: from8, to, body });
|
|
104052
|
+
}
|
|
103671
104053
|
function createDistributeAggregatedDataMessage(fields) {
|
|
103672
104054
|
const { from: from8, to, ...body } = fields;
|
|
103673
104055
|
return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from: from8, to, body });
|
|
@@ -103692,6 +104074,14 @@ a=end-of-candidates
|
|
|
103692
104074
|
const { from: from8, to, ...body } = fields;
|
|
103693
104075
|
return new BaseMessage({ type: SIGNATURE_AUTHORIZATION, from: from8, to, body });
|
|
103694
104076
|
}
|
|
104077
|
+
function createFallbackAuthorizationRequestMessage(fields) {
|
|
104078
|
+
const { from: from8, to, ...body } = fields;
|
|
104079
|
+
return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from: from8, to, body });
|
|
104080
|
+
}
|
|
104081
|
+
function createFallbackSignatureMessage(fields) {
|
|
104082
|
+
const { from: from8, to, ...body } = fields;
|
|
104083
|
+
return new BaseMessage({ type: FALLBACK_SIGNATURE, from: from8, to, body });
|
|
104084
|
+
}
|
|
103695
104085
|
|
|
103696
104086
|
// src/core/aggregation/phases.ts
|
|
103697
104087
|
init_shim();
|
|
@@ -103706,6 +104096,7 @@ a=end-of-candidates
|
|
|
103706
104096
|
ServiceCohortPhase2["SigningStarted"] = "SigningStarted";
|
|
103707
104097
|
ServiceCohortPhase2["NoncesCollected"] = "NoncesCollected";
|
|
103708
104098
|
ServiceCohortPhase2["AwaitingPartialSigs"] = "AwaitingPartialSigs";
|
|
104099
|
+
ServiceCohortPhase2["FallbackRequested"] = "FallbackRequested";
|
|
103709
104100
|
ServiceCohortPhase2["Complete"] = "Complete";
|
|
103710
104101
|
ServiceCohortPhase2["Failed"] = "Failed";
|
|
103711
104102
|
return ServiceCohortPhase2;
|
|
@@ -103715,11 +104106,13 @@ a=end-of-candidates
|
|
|
103715
104106
|
ParticipantCohortPhase2["OptedIn"] = "OptedIn";
|
|
103716
104107
|
ParticipantCohortPhase2["CohortReady"] = "CohortReady";
|
|
103717
104108
|
ParticipantCohortPhase2["UpdateSubmitted"] = "UpdateSubmitted";
|
|
104109
|
+
ParticipantCohortPhase2["NonIncluded"] = "NonIncluded";
|
|
103718
104110
|
ParticipantCohortPhase2["AwaitingValidation"] = "AwaitingValidation";
|
|
103719
104111
|
ParticipantCohortPhase2["ValidationSent"] = "ValidationSent";
|
|
103720
104112
|
ParticipantCohortPhase2["AwaitingSigning"] = "AwaitingSigning";
|
|
103721
104113
|
ParticipantCohortPhase2["NonceSent"] = "NonceSent";
|
|
103722
104114
|
ParticipantCohortPhase2["AwaitingPartialSig"] = "AwaitingPartialSig";
|
|
104115
|
+
ParticipantCohortPhase2["AwaitingFallbackSig"] = "AwaitingFallbackSig";
|
|
103723
104116
|
ParticipantCohortPhase2["Complete"] = "Complete";
|
|
103724
104117
|
ParticipantCohortPhase2["Failed"] = "Failed";
|
|
103725
104118
|
return ParticipantCohortPhase2;
|
|
@@ -103747,11 +104140,11 @@ a=end-of-candidates
|
|
|
103747
104140
|
prevOutScripts;
|
|
103748
104141
|
/** Previous output values for Taproot sighash computation. */
|
|
103749
104142
|
prevOutValues;
|
|
103750
|
-
/** Map of participant publicKey-hex
|
|
104143
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
103751
104144
|
nonceContributions = /* @__PURE__ */ new Map();
|
|
103752
104145
|
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
103753
104146
|
aggregatedNonce;
|
|
103754
|
-
/** Map of participant DID
|
|
104147
|
+
/** Map of participant DID to partial signature. */
|
|
103755
104148
|
partialSignatures = /* @__PURE__ */ new Map();
|
|
103756
104149
|
/** Final 64-byte Schnorr signature. */
|
|
103757
104150
|
signature;
|
|
@@ -103934,7 +104327,7 @@ a=end-of-candidates
|
|
|
103934
104327
|
throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
|
|
103935
104328
|
}
|
|
103936
104329
|
if (!this.#secretNonce) {
|
|
103937
|
-
throw new SigningSessionError("Secret nonce not available
|
|
104330
|
+
throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
|
|
103938
104331
|
}
|
|
103939
104332
|
const session = new Session(
|
|
103940
104333
|
this.aggregatedNonce,
|
|
@@ -104004,6 +104397,9 @@ a=end-of-candidates
|
|
|
104004
104397
|
case SUBMIT_UPDATE:
|
|
104005
104398
|
this.#handleSubmitUpdate(message2);
|
|
104006
104399
|
break;
|
|
104400
|
+
case SUBMIT_NONINCLUDED:
|
|
104401
|
+
this.#handleSubmitNonInclusion(message2);
|
|
104402
|
+
break;
|
|
104007
104403
|
case VALIDATION_ACK:
|
|
104008
104404
|
this.#handleValidationAck(message2);
|
|
104009
104405
|
break;
|
|
@@ -104013,6 +104409,9 @@ a=end-of-candidates
|
|
|
104013
104409
|
case SIGNATURE_AUTHORIZATION:
|
|
104014
104410
|
this.#handleSignatureAuthorization(message2);
|
|
104015
104411
|
break;
|
|
104412
|
+
case FALLBACK_SIGNATURE:
|
|
104413
|
+
this.#handleFallbackSignature(message2);
|
|
104414
|
+
break;
|
|
104016
104415
|
default:
|
|
104017
104416
|
break;
|
|
104018
104417
|
}
|
|
@@ -104031,7 +104430,7 @@ a=end-of-candidates
|
|
|
104031
104430
|
}
|
|
104032
104431
|
/**
|
|
104033
104432
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
104034
|
-
* Cohort starts in `Created` phase
|
|
104433
|
+
* Cohort starts in `Created` phase - call `advertise()` to broadcast.
|
|
104035
104434
|
*/
|
|
104036
104435
|
createCohort(config) {
|
|
104037
104436
|
const problems = validateCohortConditions(config);
|
|
@@ -104046,7 +104445,11 @@ a=end-of-candidates
|
|
|
104046
104445
|
serviceDid: this.did,
|
|
104047
104446
|
minParticipants: config.minParticipants,
|
|
104048
104447
|
network: config.network,
|
|
104049
|
-
beaconType: config.beaconType
|
|
104448
|
+
beaconType: config.beaconType,
|
|
104449
|
+
recoveryKey: hexToBytes(config.recoveryKey),
|
|
104450
|
+
recoverySequence: config.recoverySequence,
|
|
104451
|
+
fundingModel: config.fundingModel,
|
|
104452
|
+
fallbackThreshold: config.fallbackThreshold
|
|
104050
104453
|
});
|
|
104051
104454
|
this.#cohortStates.set(cohort.id, {
|
|
104052
104455
|
phase: "Created" /* Created */,
|
|
@@ -104247,11 +104650,64 @@ a=end-of-candidates
|
|
|
104247
104650
|
});
|
|
104248
104651
|
return;
|
|
104249
104652
|
}
|
|
104653
|
+
if (state.cohort.nonIncluded.has(message2.from)) {
|
|
104654
|
+
state.rejections.push({
|
|
104655
|
+
from: message2.from,
|
|
104656
|
+
code: "UPDATE_MALFORMED",
|
|
104657
|
+
reason: "Participant already declined this round; cannot also submit an update"
|
|
104658
|
+
});
|
|
104659
|
+
return;
|
|
104660
|
+
}
|
|
104661
|
+
if (state.cohort.pendingUpdates.has(message2.from)) {
|
|
104662
|
+
state.rejections.push({
|
|
104663
|
+
from: message2.from,
|
|
104664
|
+
code: "UPDATE_MALFORMED",
|
|
104665
|
+
reason: "Participant already submitted an update this round; cannot resubmit"
|
|
104666
|
+
});
|
|
104667
|
+
return;
|
|
104668
|
+
}
|
|
104250
104669
|
state.cohort.addUpdate(message2.from, signedUpdate);
|
|
104251
104670
|
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104252
104671
|
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104253
104672
|
}
|
|
104254
|
-
if (state.cohort.
|
|
104673
|
+
if (state.cohort.hasAllResponses()) {
|
|
104674
|
+
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104675
|
+
}
|
|
104676
|
+
}
|
|
104677
|
+
/**
|
|
104678
|
+
* Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
|
|
104679
|
+
* update this round (cooperative non-inclusion). Membership is proven by the
|
|
104680
|
+
* signed transport envelope, so the body carries only the cohortId. The member
|
|
104681
|
+
* stays in the cohort and still signs; it is absent from the CAS map and
|
|
104682
|
+
* carries a non-inclusion leaf in the SMT.
|
|
104683
|
+
*/
|
|
104684
|
+
#handleSubmitNonInclusion(message2) {
|
|
104685
|
+
const cohortId = message2.body?.cohortId;
|
|
104686
|
+
if (!cohortId) return;
|
|
104687
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104688
|
+
if (!state) return;
|
|
104689
|
+
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
104690
|
+
if (!state.cohort.participants.includes(message2.from)) {
|
|
104691
|
+
state.rejections.push({
|
|
104692
|
+
from: message2.from,
|
|
104693
|
+
code: "UPDATE_MALFORMED",
|
|
104694
|
+
reason: "Sender is not a member of this cohort"
|
|
104695
|
+
});
|
|
104696
|
+
return;
|
|
104697
|
+
}
|
|
104698
|
+
if (state.cohort.pendingUpdates.has(message2.from) || state.cohort.nonIncluded.has(message2.from)) {
|
|
104699
|
+
state.rejections.push({
|
|
104700
|
+
from: message2.from,
|
|
104701
|
+
code: "UPDATE_MALFORMED",
|
|
104702
|
+
reason: "Participant already responded this round"
|
|
104703
|
+
});
|
|
104704
|
+
return;
|
|
104705
|
+
}
|
|
104706
|
+
state.cohort.addNonInclusion(message2.from);
|
|
104707
|
+
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104708
|
+
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104709
|
+
}
|
|
104710
|
+
if (state.cohort.hasAllResponses()) {
|
|
104255
104711
|
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104256
104712
|
}
|
|
104257
104713
|
}
|
|
@@ -104361,7 +104817,7 @@ a=end-of-candidates
|
|
|
104361
104817
|
}
|
|
104362
104818
|
/**
|
|
104363
104819
|
* Start a signing session by creating auth requests for all participants.
|
|
104364
|
-
* The caller provides the transaction data
|
|
104820
|
+
* The caller provides the transaction data - typically built via
|
|
104365
104821
|
* `buildBeaconTransaction()` against a Bitcoin connection.
|
|
104366
104822
|
*/
|
|
104367
104823
|
startSigning(cohortId, txData) {
|
|
@@ -104468,11 +104924,144 @@ a=end-of-candidates
|
|
|
104468
104924
|
state.result = {
|
|
104469
104925
|
cohortId,
|
|
104470
104926
|
signature,
|
|
104471
|
-
signedTx: state.signingSession.pendingTx
|
|
104927
|
+
signedTx: state.signingSession.pendingTx,
|
|
104928
|
+
path: "key-path"
|
|
104472
104929
|
};
|
|
104473
104930
|
state.phase = "Complete" /* Complete */;
|
|
104474
104931
|
}
|
|
104475
104932
|
}
|
|
104933
|
+
/**
|
|
104934
|
+
* Abandon the optimistic n-of-n key path and ask members to authorize the
|
|
104935
|
+
* k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
|
|
104936
|
+
* liveness, ADR 042). Reuses the in-flight signing session's transaction and
|
|
104937
|
+
* spent output, so the announcement and its outputs are unchanged: only the
|
|
104938
|
+
* witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
|
|
104939
|
+
* participant.
|
|
104940
|
+
*
|
|
104941
|
+
* Callable once optimistic signing has started (the session and its tx exist)
|
|
104942
|
+
* and before it completes. A cohort can take exactly one of the two paths: the
|
|
104943
|
+
* caller (runner) must commit to fallback and stop driving the optimistic path.
|
|
104944
|
+
*/
|
|
104945
|
+
startFallbackSigning(cohortId) {
|
|
104946
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104947
|
+
if (!state) {
|
|
104948
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
104949
|
+
}
|
|
104950
|
+
if (!state.signingSession) {
|
|
104951
|
+
throw new AggregationServiceError(
|
|
104952
|
+
`Cannot start fallback for cohort ${cohortId}: no signing session.`,
|
|
104953
|
+
"NO_SIGNING_SESSION",
|
|
104954
|
+
{ cohortId }
|
|
104955
|
+
);
|
|
104956
|
+
}
|
|
104957
|
+
const signingPhases = [
|
|
104958
|
+
"SigningStarted" /* SigningStarted */,
|
|
104959
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
104960
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
104961
|
+
];
|
|
104962
|
+
if (!signingPhases.includes(state.phase)) {
|
|
104963
|
+
throw new AggregationServiceError(
|
|
104964
|
+
`Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
104965
|
+
"INVALID_PHASE",
|
|
104966
|
+
{ cohortId, phase: state.phase }
|
|
104967
|
+
);
|
|
104968
|
+
}
|
|
104969
|
+
const session = state.signingSession;
|
|
104970
|
+
const prevOutScript = session.prevOutScripts[0];
|
|
104971
|
+
const prevOutValue = session.prevOutValues[0];
|
|
104972
|
+
if (!prevOutScript || prevOutValue === void 0) {
|
|
104973
|
+
throw new AggregationServiceError(
|
|
104974
|
+
`Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
|
|
104975
|
+
"MISSING_PREV_OUT",
|
|
104976
|
+
{ cohortId }
|
|
104977
|
+
);
|
|
104978
|
+
}
|
|
104979
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
104980
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
104981
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
104982
|
+
});
|
|
104983
|
+
state.fallbackSignatures = /* @__PURE__ */ new Map();
|
|
104984
|
+
state.phase = "FallbackRequested" /* FallbackRequested */;
|
|
104985
|
+
const messages2 = [];
|
|
104986
|
+
for (const participantDid of state.cohort.participants) {
|
|
104987
|
+
messages2.push(createFallbackAuthorizationRequestMessage({
|
|
104988
|
+
from: this.did,
|
|
104989
|
+
to: participantDid,
|
|
104990
|
+
cohortId,
|
|
104991
|
+
sessionId: session.id,
|
|
104992
|
+
pendingTx: session.pendingTx.hex,
|
|
104993
|
+
prevOutScriptHex: bytesToHex(prevOutScript),
|
|
104994
|
+
prevOutValue: prevOutValue.toString(),
|
|
104995
|
+
fallbackLeafScriptHex: bytesToHex(fallbackLeaf)
|
|
104996
|
+
}));
|
|
104997
|
+
}
|
|
104998
|
+
return messages2;
|
|
104999
|
+
}
|
|
105000
|
+
/**
|
|
105001
|
+
* Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
|
|
105002
|
+
* signature over the fallback script-path sighash. The signature is
|
|
105003
|
+
* authenticated to the sender (its `signerPk` must be the sender's own cohort
|
|
105004
|
+
* key and the signature must verify against the sighash) and collected. Once k
|
|
105005
|
+
* valid signatures are in, the k-of-n fallback spend is assembled and the
|
|
105006
|
+
* cohort completes via the script path.
|
|
105007
|
+
*/
|
|
105008
|
+
#handleFallbackSignature(message2) {
|
|
105009
|
+
const cohortId = message2.body?.cohortId;
|
|
105010
|
+
if (!cohortId) return;
|
|
105011
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105012
|
+
if (!state || !state.signingSession || !state.fallbackSignatures) return;
|
|
105013
|
+
if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
|
|
105014
|
+
const sessionId = message2.body?.sessionId;
|
|
105015
|
+
if (sessionId !== state.signingSession.id) return;
|
|
105016
|
+
const signerPk = message2.body?.signerPk;
|
|
105017
|
+
const fallbackSignature = message2.body?.fallbackSignature;
|
|
105018
|
+
if (!signerPk || !fallbackSignature) return;
|
|
105019
|
+
const prevOutScript = state.signingSession.prevOutScripts[0];
|
|
105020
|
+
const prevOutValue = state.signingSession.prevOutValues[0];
|
|
105021
|
+
if (!prevOutScript || prevOutValue === void 0) return;
|
|
105022
|
+
const memberKey = state.cohort.participantKeys.get(message2.from);
|
|
105023
|
+
if (!memberKey) {
|
|
105024
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
|
|
105025
|
+
return;
|
|
105026
|
+
}
|
|
105027
|
+
const memberXOnly = memberKey.slice(1);
|
|
105028
|
+
if (signerPk.length !== 32 || !memberXOnly.every((b, i4) => b === signerPk[i4])) {
|
|
105029
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
|
|
105030
|
+
return;
|
|
105031
|
+
}
|
|
105032
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
105033
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105034
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
105035
|
+
});
|
|
105036
|
+
const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
105037
|
+
let valid = false;
|
|
105038
|
+
try {
|
|
105039
|
+
valid = fallbackSignature.length === 64 && schnorr2.verify(fallbackSignature, sighash, signerPk);
|
|
105040
|
+
} catch {
|
|
105041
|
+
valid = false;
|
|
105042
|
+
}
|
|
105043
|
+
if (!valid) {
|
|
105044
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
|
|
105045
|
+
return;
|
|
105046
|
+
}
|
|
105047
|
+
state.fallbackSignatures.set(message2.from, { pubKey: signerPk, signature: fallbackSignature });
|
|
105048
|
+
if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
|
|
105049
|
+
const signedTx = buildFallbackSpend({
|
|
105050
|
+
pendingTx: state.signingSession.pendingTx,
|
|
105051
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105052
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold,
|
|
105053
|
+
recoveryKey: state.cohort.recoveryKey,
|
|
105054
|
+
recoverySequence: state.cohort.recoverySequence,
|
|
105055
|
+
fundingModel: state.cohort.fundingModel,
|
|
105056
|
+
network: state.cohort.network,
|
|
105057
|
+
prevOutScript,
|
|
105058
|
+
prevOutValue,
|
|
105059
|
+
signatures: [...state.fallbackSignatures.values()]
|
|
105060
|
+
});
|
|
105061
|
+
state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
|
|
105062
|
+
state.phase = "Complete" /* Complete */;
|
|
105063
|
+
}
|
|
105064
|
+
}
|
|
104476
105065
|
getResult(cohortId) {
|
|
104477
105066
|
return this.#cohortStates.get(cohortId)?.result;
|
|
104478
105067
|
}
|
|
@@ -104504,6 +105093,7 @@ a=end-of-candidates
|
|
|
104504
105093
|
|
|
104505
105094
|
// src/core/aggregation/participant.ts
|
|
104506
105095
|
init_shim();
|
|
105096
|
+
init_secp256k1();
|
|
104507
105097
|
init_utils();
|
|
104508
105098
|
|
|
104509
105099
|
// src/core/aggregation/messages/bodies.ts
|
|
@@ -104517,6 +105107,14 @@ a=end-of-candidates
|
|
|
104517
105107
|
const v = b ? b[k] : void 0;
|
|
104518
105108
|
return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
104519
105109
|
};
|
|
105110
|
+
var intInRange = (b, k, min, max) => {
|
|
105111
|
+
const v = b ? b[k] : void 0;
|
|
105112
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
105113
|
+
};
|
|
105114
|
+
var optStrOneOf = (b, k, allowed) => {
|
|
105115
|
+
const v = b ? b[k] : void 0;
|
|
105116
|
+
return v === void 0 || typeof v === "string" && allowed.includes(v);
|
|
105117
|
+
};
|
|
104520
105118
|
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
104521
105119
|
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
104522
105120
|
var hasBytesArray = (b, k) => {
|
|
@@ -104524,7 +105122,7 @@ a=end-of-candidates
|
|
|
104524
105122
|
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
104525
105123
|
};
|
|
104526
105124
|
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");
|
|
105125
|
+
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
105126
|
}
|
|
104529
105127
|
function isCohortOptInMessage(m2) {
|
|
104530
105128
|
return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
|
|
@@ -104538,6 +105136,9 @@ a=end-of-candidates
|
|
|
104538
105136
|
function isSubmitUpdateMessage(m2) {
|
|
104539
105137
|
return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
|
|
104540
105138
|
}
|
|
105139
|
+
function isSubmitNonIncludedMessage(m2) {
|
|
105140
|
+
return m2.type === SUBMIT_NONINCLUDED && hasStr(m2.body, "cohortId");
|
|
105141
|
+
}
|
|
104541
105142
|
function isDistributeAggregatedDataMessage(m2) {
|
|
104542
105143
|
return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
|
|
104543
105144
|
}
|
|
@@ -104556,8 +105157,38 @@ a=end-of-candidates
|
|
|
104556
105157
|
function isSignatureAuthorizationMessage(m2) {
|
|
104557
105158
|
return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
|
|
104558
105159
|
}
|
|
105160
|
+
function isFallbackAuthorizationRequestMessage(m2) {
|
|
105161
|
+
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");
|
|
105162
|
+
}
|
|
105163
|
+
function isFallbackSignatureMessage(m2) {
|
|
105164
|
+
return m2.type === FALLBACK_SIGNATURE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "signerPk") && hasBytes(m2.body, "fallbackSignature");
|
|
105165
|
+
}
|
|
104559
105166
|
|
|
104560
105167
|
// src/core/aggregation/participant.ts
|
|
105168
|
+
function txEmbedsSignal(tx, signalHex) {
|
|
105169
|
+
let expected;
|
|
105170
|
+
try {
|
|
105171
|
+
expected = hexToBytes(signalHex);
|
|
105172
|
+
} catch {
|
|
105173
|
+
return false;
|
|
105174
|
+
}
|
|
105175
|
+
if (expected.length === 0) return false;
|
|
105176
|
+
for (let i4 = 0; i4 < tx.outputsLength; i4++) {
|
|
105177
|
+
const script = tx.getOutput(i4)?.script;
|
|
105178
|
+
if (!script) continue;
|
|
105179
|
+
let decoded;
|
|
105180
|
+
try {
|
|
105181
|
+
decoded = Script.decode(script);
|
|
105182
|
+
} catch {
|
|
105183
|
+
continue;
|
|
105184
|
+
}
|
|
105185
|
+
if (decoded.length === 2 && decoded[0] === "RETURN" && decoded[1] instanceof Uint8Array) {
|
|
105186
|
+
const payload = decoded[1];
|
|
105187
|
+
if (payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
|
|
105188
|
+
}
|
|
105189
|
+
}
|
|
105190
|
+
return false;
|
|
105191
|
+
}
|
|
104561
105192
|
var AggregationParticipant = class {
|
|
104562
105193
|
did;
|
|
104563
105194
|
/** MuSig2 signing capability. The raw secret never lives as a field here. */
|
|
@@ -104574,7 +105205,7 @@ a=end-of-candidates
|
|
|
104574
105205
|
}
|
|
104575
105206
|
/**
|
|
104576
105207
|
* Process an incoming message. Updates internal state but never produces
|
|
104577
|
-
* outgoing messages
|
|
105208
|
+
* outgoing messages: those come exclusively from action methods.
|
|
104578
105209
|
*/
|
|
104579
105210
|
receive(message2) {
|
|
104580
105211
|
if (message2.version === void 0 || message2.version !== AGGREGATION_WIRE_VERSION) {
|
|
@@ -104600,6 +105231,9 @@ a=end-of-candidates
|
|
|
104600
105231
|
case AGGREGATED_NONCE:
|
|
104601
105232
|
this.#handleAggregatedNonce(message2);
|
|
104602
105233
|
break;
|
|
105234
|
+
case FALLBACK_AUTHORIZATION_REQUEST:
|
|
105235
|
+
this.#handleFallbackAuthorizationRequest(message2);
|
|
105236
|
+
break;
|
|
104603
105237
|
default:
|
|
104604
105238
|
break;
|
|
104605
105239
|
}
|
|
@@ -104645,12 +105279,17 @@ a=end-of-candidates
|
|
|
104645
105279
|
{ cohortId, phase: state?.phase }
|
|
104646
105280
|
);
|
|
104647
105281
|
}
|
|
105282
|
+
const advert = state.advert;
|
|
104648
105283
|
const cohort = new AggregationCohort({
|
|
104649
105284
|
id: cohortId,
|
|
104650
105285
|
serviceDid: state.serviceDid,
|
|
104651
|
-
minParticipants:
|
|
104652
|
-
network:
|
|
104653
|
-
beaconType:
|
|
105286
|
+
minParticipants: advert.minParticipants,
|
|
105287
|
+
network: advert.network,
|
|
105288
|
+
beaconType: advert.beaconType,
|
|
105289
|
+
recoveryKey: advert.recoveryKey ? hexToBytes(advert.recoveryKey) : void 0,
|
|
105290
|
+
recoverySequence: advert.recoverySequence,
|
|
105291
|
+
fundingModel: advert.fundingModel,
|
|
105292
|
+
fallbackThreshold: advert.fallbackThreshold
|
|
104654
105293
|
});
|
|
104655
105294
|
state.cohort = cohort;
|
|
104656
105295
|
state.phase = "OptedIn" /* OptedIn */;
|
|
@@ -104665,7 +105304,7 @@ a=end-of-candidates
|
|
|
104665
105304
|
}
|
|
104666
105305
|
#handleOptInAccept(message2) {
|
|
104667
105306
|
}
|
|
104668
|
-
/** Cohorts that have been finalized
|
|
105307
|
+
/** Cohorts that have been finalized: beacon address available. */
|
|
104669
105308
|
get joinedCohorts() {
|
|
104670
105309
|
const map3 = /* @__PURE__ */ new Map();
|
|
104671
105310
|
for (const [id, state] of this.#cohortStates) {
|
|
@@ -104708,6 +105347,7 @@ a=end-of-candidates
|
|
|
104708
105347
|
);
|
|
104709
105348
|
}
|
|
104710
105349
|
state.submittedUpdate = signedUpdate;
|
|
105350
|
+
state.included = true;
|
|
104711
105351
|
state.phase = "UpdateSubmitted" /* UpdateSubmitted */;
|
|
104712
105352
|
const message2 = createSubmitUpdateMessage({
|
|
104713
105353
|
from: this.did,
|
|
@@ -104717,6 +105357,30 @@ a=end-of-candidates
|
|
|
104717
105357
|
});
|
|
104718
105358
|
return [message2];
|
|
104719
105359
|
}
|
|
105360
|
+
/**
|
|
105361
|
+
* User action: decline to submit an update this round (cooperative
|
|
105362
|
+
* non-inclusion). The member stays in the cohort and still signs; it will be
|
|
105363
|
+
* absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
|
|
105364
|
+
* SMT. Returns the SUBMIT_NONINCLUDED message to send.
|
|
105365
|
+
*/
|
|
105366
|
+
declineUpdate(cohortId) {
|
|
105367
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105368
|
+
if (!state || state.phase !== "CohortReady" /* CohortReady */) {
|
|
105369
|
+
throw new AggregationParticipantError(
|
|
105370
|
+
`Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
|
|
105371
|
+
"INVALID_PHASE",
|
|
105372
|
+
{ cohortId, phase: state?.phase }
|
|
105373
|
+
);
|
|
105374
|
+
}
|
|
105375
|
+
state.included = false;
|
|
105376
|
+
state.phase = "NonIncluded" /* NonIncluded */;
|
|
105377
|
+
const message2 = createSubmitNonIncludedMessage({
|
|
105378
|
+
from: this.did,
|
|
105379
|
+
to: state.serviceDid,
|
|
105380
|
+
cohortId
|
|
105381
|
+
});
|
|
105382
|
+
return [message2];
|
|
105383
|
+
}
|
|
104720
105384
|
/** Aggregated data awaiting user validation. */
|
|
104721
105385
|
get pendingValidations() {
|
|
104722
105386
|
const map3 = /* @__PURE__ */ new Map();
|
|
@@ -104730,9 +105394,9 @@ a=end-of-candidates
|
|
|
104730
105394
|
/**
|
|
104731
105395
|
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
104732
105396
|
* 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
|
-
*
|
|
105397
|
+
* the validate decision), this returns the stored validation, including the
|
|
105398
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
|
|
105399
|
+
* so it is still readable once the cohort reaches Complete. Returns
|
|
104736
105400
|
* undefined before aggregated data has been received.
|
|
104737
105401
|
*/
|
|
104738
105402
|
getValidation(cohortId) {
|
|
@@ -104742,18 +105406,20 @@ a=end-of-candidates
|
|
|
104742
105406
|
const cohortId = message2.body?.cohortId;
|
|
104743
105407
|
if (!cohortId) return;
|
|
104744
105408
|
const state = this.#cohortStates.get(cohortId);
|
|
104745
|
-
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
|
|
104746
|
-
|
|
105409
|
+
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */ && state.phase !== "NonIncluded" /* NonIncluded */) return;
|
|
105410
|
+
const declined = state.included === false;
|
|
105411
|
+
if (!declined && !state.submittedUpdate) return;
|
|
104747
105412
|
const beaconType = message2.body?.beaconType;
|
|
104748
105413
|
if (!beaconType) return;
|
|
104749
105414
|
const strategy = getBeaconStrategy(beaconType);
|
|
104750
105415
|
if (!strategy) return;
|
|
104751
105416
|
const signalBytesHex = message2.body?.signalBytesHex ?? "";
|
|
104752
|
-
const expectedHash = canonicalHash(state.submittedUpdate);
|
|
105417
|
+
const expectedHash = declined ? "" : canonicalHash(state.submittedUpdate);
|
|
104753
105418
|
const result = strategy.validateParticipantView({
|
|
104754
105419
|
participantDid: this.did,
|
|
104755
|
-
|
|
104756
|
-
|
|
105420
|
+
included: !declined,
|
|
105421
|
+
submittedUpdate: declined ? void 0 : state.submittedUpdate,
|
|
105422
|
+
expectedHash: declined ? void 0 : expectedHash,
|
|
104757
105423
|
body: message2.body
|
|
104758
105424
|
});
|
|
104759
105425
|
state.validation = {
|
|
@@ -104763,7 +105429,8 @@ a=end-of-candidates
|
|
|
104763
105429
|
expectedHash,
|
|
104764
105430
|
matches: result.matches,
|
|
104765
105431
|
casAnnouncement: result.casAnnouncement,
|
|
104766
|
-
smtProof: result.smtProof
|
|
105432
|
+
smtProof: result.smtProof,
|
|
105433
|
+
included: !declined
|
|
104767
105434
|
};
|
|
104768
105435
|
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
104769
105436
|
}
|
|
@@ -104837,6 +105504,31 @@ a=end-of-candidates
|
|
|
104837
105504
|
};
|
|
104838
105505
|
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
104839
105506
|
}
|
|
105507
|
+
/**
|
|
105508
|
+
* Bind a signing approval to the announcement the member validated: a beacon
|
|
105509
|
+
* transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
|
|
105510
|
+
* the aggregated data was distributed. Both the optimistic nonce approval and
|
|
105511
|
+
* the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
|
|
105512
|
+
* while the coordinator drives output selection, so without this check a
|
|
105513
|
+
* coordinator could anchor a different signal under the member's signature.
|
|
105514
|
+
*/
|
|
105515
|
+
#assertTxAnchorsValidatedSignal(cohortId, state, tx) {
|
|
105516
|
+
const signalHex = state.validation?.signalBytesHex;
|
|
105517
|
+
if (!signalHex) {
|
|
105518
|
+
throw new AggregationParticipantError(
|
|
105519
|
+
`Cohort ${cohortId} has no validated signal to bind the signature to.`,
|
|
105520
|
+
"MISSING_STATE",
|
|
105521
|
+
{ cohortId }
|
|
105522
|
+
);
|
|
105523
|
+
}
|
|
105524
|
+
if (!txEmbedsSignal(tx, signalHex)) {
|
|
105525
|
+
throw new AggregationParticipantError(
|
|
105526
|
+
`Transaction for cohort ${cohortId} does not anchor the validated signal.`,
|
|
105527
|
+
"SIGNAL_MISMATCH",
|
|
105528
|
+
{ cohortId }
|
|
105529
|
+
);
|
|
105530
|
+
}
|
|
105531
|
+
}
|
|
104840
105532
|
/**
|
|
104841
105533
|
* User action: approve signing and generate nonce contribution.
|
|
104842
105534
|
*/
|
|
@@ -104856,7 +105548,8 @@ a=end-of-candidates
|
|
|
104856
105548
|
{ cohortId }
|
|
104857
105549
|
);
|
|
104858
105550
|
}
|
|
104859
|
-
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
|
|
105551
|
+
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
|
|
105552
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
104860
105553
|
const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
|
|
104861
105554
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
104862
105555
|
const session = new BeaconSigningSession({
|
|
@@ -104892,7 +105585,7 @@ a=end-of-candidates
|
|
|
104892
105585
|
}
|
|
104893
105586
|
/**
|
|
104894
105587
|
* User action: generate and return the partial signature.
|
|
104895
|
-
* In most UIs this is automatic after AwaitingPartialSig
|
|
105588
|
+
* In most UIs this is automatic after AwaitingPartialSig, but exposing it
|
|
104896
105589
|
* as an explicit action lets the client UI confirm before signing if desired.
|
|
104897
105590
|
*/
|
|
104898
105591
|
generatePartialSignature(cohortId) {
|
|
@@ -104924,6 +105617,83 @@ a=end-of-candidates
|
|
|
104924
105617
|
partialSignature: partialSig
|
|
104925
105618
|
})];
|
|
104926
105619
|
}
|
|
105620
|
+
/** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
|
|
105621
|
+
get pendingFallbackRequests() {
|
|
105622
|
+
const map3 = /* @__PURE__ */ new Map();
|
|
105623
|
+
for (const [id, state] of this.#cohortStates) {
|
|
105624
|
+
if (state.phase === "AwaitingFallbackSig" /* AwaitingFallbackSig */ && state.fallbackRequest) {
|
|
105625
|
+
map3.set(id, state.fallbackRequest);
|
|
105626
|
+
}
|
|
105627
|
+
}
|
|
105628
|
+
return map3;
|
|
105629
|
+
}
|
|
105630
|
+
#handleFallbackAuthorizationRequest(message2) {
|
|
105631
|
+
const cohortId = message2.body?.cohortId;
|
|
105632
|
+
if (!cohortId) return;
|
|
105633
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105634
|
+
if (!state || !state.cohort) return;
|
|
105635
|
+
const acceptFrom = [
|
|
105636
|
+
"ValidationSent" /* ValidationSent */,
|
|
105637
|
+
"AwaitingSigning" /* AwaitingSigning */,
|
|
105638
|
+
"NonceSent" /* NonceSent */,
|
|
105639
|
+
"AwaitingPartialSig" /* AwaitingPartialSig */,
|
|
105640
|
+
"Complete" /* Complete */
|
|
105641
|
+
];
|
|
105642
|
+
if (!acceptFrom.includes(state.phase)) return;
|
|
105643
|
+
const sessionId = message2.body?.sessionId;
|
|
105644
|
+
const pendingTxHex = message2.body?.pendingTx;
|
|
105645
|
+
const prevOutScriptHex = message2.body?.prevOutScriptHex;
|
|
105646
|
+
const prevOutValue = message2.body?.prevOutValue;
|
|
105647
|
+
const fallbackLeafScriptHex = message2.body?.fallbackLeafScriptHex;
|
|
105648
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
|
|
105649
|
+
state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
|
|
105650
|
+
state.signingSession?.clearSecrets();
|
|
105651
|
+
state.phase = "AwaitingFallbackSig" /* AwaitingFallbackSig */;
|
|
105652
|
+
}
|
|
105653
|
+
/**
|
|
105654
|
+
* User action: authorize the fallback spend. Recomputes the k-of-n fallback
|
|
105655
|
+
* leaf from the member's OWN cohort state (not the service-provided script),
|
|
105656
|
+
* computes the BIP-341 script-path sighash over the requested transaction, and
|
|
105657
|
+
* returns a standalone BIP-340 signature (no nonce round). The member completes
|
|
105658
|
+
* once it has contributed; the service needs only k of these.
|
|
105659
|
+
*/
|
|
105660
|
+
approveFallback(cohortId) {
|
|
105661
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105662
|
+
if (!state || state.phase !== "AwaitingFallbackSig" /* AwaitingFallbackSig */) {
|
|
105663
|
+
throw new AggregationParticipantError(
|
|
105664
|
+
`Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
|
|
105665
|
+
"INVALID_PHASE",
|
|
105666
|
+
{ cohortId, phase: state?.phase }
|
|
105667
|
+
);
|
|
105668
|
+
}
|
|
105669
|
+
if (!state.fallbackRequest || !state.cohort) {
|
|
105670
|
+
throw new AggregationParticipantError(
|
|
105671
|
+
`Cohort ${cohortId} missing fallback request or cohort state.`,
|
|
105672
|
+
"MISSING_STATE",
|
|
105673
|
+
{ cohortId }
|
|
105674
|
+
);
|
|
105675
|
+
}
|
|
105676
|
+
const req = state.fallbackRequest;
|
|
105677
|
+
const tx = Transaction.fromRaw(hexToBytes(req.pendingTxHex), { allowUnknownOutputs: true });
|
|
105678
|
+
const prevOutScript = hexToBytes(req.prevOutScriptHex);
|
|
105679
|
+
const prevOutValue = BigInt(req.prevOutValue);
|
|
105680
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
105681
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
105682
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105683
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
105684
|
+
});
|
|
105685
|
+
const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
105686
|
+
const signature = this.#signer.withSecret((secretKey) => schnorr2.sign(sighash, secretKey));
|
|
105687
|
+
state.phase = "Complete" /* Complete */;
|
|
105688
|
+
return [createFallbackSignatureMessage({
|
|
105689
|
+
from: this.did,
|
|
105690
|
+
to: state.serviceDid,
|
|
105691
|
+
cohortId,
|
|
105692
|
+
sessionId: req.sessionId,
|
|
105693
|
+
signerPk: this.publicKey.slice(1),
|
|
105694
|
+
fallbackSignature: signature
|
|
105695
|
+
})];
|
|
105696
|
+
}
|
|
104927
105697
|
getCohortPhase(cohortId) {
|
|
104928
105698
|
return this.#cohortStates.get(cohortId)?.phase;
|
|
104929
105699
|
}
|
|
@@ -104960,6 +105730,85 @@ a=end-of-candidates
|
|
|
104960
105730
|
}
|
|
104961
105731
|
};
|
|
104962
105732
|
|
|
105733
|
+
// src/core/aggregation/recovery-spend.ts
|
|
105734
|
+
init_shim();
|
|
105735
|
+
init_secp256k1();
|
|
105736
|
+
var DUST_LIMIT_SATS = 546n;
|
|
105737
|
+
function bytesEqual2(a2, b) {
|
|
105738
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
105739
|
+
}
|
|
105740
|
+
function buildRecoverySpend(params) {
|
|
105741
|
+
const {
|
|
105742
|
+
cohortKeys,
|
|
105743
|
+
recoverySecretKey,
|
|
105744
|
+
recoveryKey,
|
|
105745
|
+
recoverySequence,
|
|
105746
|
+
fallbackThreshold,
|
|
105747
|
+
fundingModel,
|
|
105748
|
+
network,
|
|
105749
|
+
utxo,
|
|
105750
|
+
destinationAddress,
|
|
105751
|
+
fee,
|
|
105752
|
+
beaconAddress
|
|
105753
|
+
} = params;
|
|
105754
|
+
if (cohortKeys.length === 0) {
|
|
105755
|
+
throw new AggregationCohortError(
|
|
105756
|
+
"Cannot build recovery spend: no cohort keys.",
|
|
105757
|
+
"NO_COHORT_KEYS"
|
|
105758
|
+
);
|
|
105759
|
+
}
|
|
105760
|
+
const derivedPub = schnorr2.getPublicKey(recoverySecretKey);
|
|
105761
|
+
if (!bytesEqual2(derivedPub, recoveryKey)) {
|
|
105762
|
+
throw new AggregationCohortError(
|
|
105763
|
+
"Recovery secret key does not correspond to the committed recovery key.",
|
|
105764
|
+
"RECOVERY_KEY_MISMATCH"
|
|
105765
|
+
);
|
|
105766
|
+
}
|
|
105767
|
+
const out = utxo.value - fee;
|
|
105768
|
+
if (out <= 0n) {
|
|
105769
|
+
throw new AggregationCohortError(
|
|
105770
|
+
`Recovery fee ${fee} exceeds UTXO value ${utxo.value}.`,
|
|
105771
|
+
"FEE_EXCEEDS_VALUE",
|
|
105772
|
+
{ fee: fee.toString(), value: utxo.value.toString() }
|
|
105773
|
+
);
|
|
105774
|
+
}
|
|
105775
|
+
if (out < DUST_LIMIT_SATS) {
|
|
105776
|
+
throw new AggregationCohortError(
|
|
105777
|
+
`Recovered output ${out} is below the dust limit ${DUST_LIMIT_SATS}; the spend would not relay.`,
|
|
105778
|
+
"DUST_OUTPUT",
|
|
105779
|
+
{ output: out.toString(), dustLimit: DUST_LIMIT_SATS.toString() }
|
|
105780
|
+
);
|
|
105781
|
+
}
|
|
105782
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
105783
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
105784
|
+
recoveryKey,
|
|
105785
|
+
recoverySequence,
|
|
105786
|
+
cohortKeys,
|
|
105787
|
+
fallbackThreshold: resolveFallbackThreshold(fallbackThreshold, cohortKeys.length)
|
|
105788
|
+
});
|
|
105789
|
+
const net = getNetwork(network);
|
|
105790
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
105791
|
+
if (beaconAddress !== void 0 && payment.address !== beaconAddress) {
|
|
105792
|
+
throw new AggregationCohortError(
|
|
105793
|
+
`Reconstructed beacon address ${payment.address} does not match the cohort's funded address ${beaconAddress}.`,
|
|
105794
|
+
"BEACON_ADDRESS_MISMATCH",
|
|
105795
|
+
{ reconstructed: payment.address, expected: beaconAddress }
|
|
105796
|
+
);
|
|
105797
|
+
}
|
|
105798
|
+
const tx = new Transaction({ version: 2, allowUnknownInputs: true });
|
|
105799
|
+
tx.addInput({
|
|
105800
|
+
txid: utxo.txid,
|
|
105801
|
+
index: utxo.vout,
|
|
105802
|
+
witnessUtxo: { script: payment.script, amount: utxo.value },
|
|
105803
|
+
tapLeafScript: payment.tapLeafScript,
|
|
105804
|
+
sequence: recoverySequence
|
|
105805
|
+
});
|
|
105806
|
+
tx.addOutputAddress(destinationAddress, out, net);
|
|
105807
|
+
tx.signIdx(recoverySecretKey, 0);
|
|
105808
|
+
tx.finalize();
|
|
105809
|
+
return tx;
|
|
105810
|
+
}
|
|
105811
|
+
|
|
104963
105812
|
// src/core/aggregation/logger.ts
|
|
104964
105813
|
init_shim();
|
|
104965
105814
|
var CONSOLE_LOGGER = {
|
|
@@ -104992,6 +105841,7 @@ a=end-of-candidates
|
|
|
104992
105841
|
]);
|
|
104993
105842
|
var UPDATE_VALUES = /* @__PURE__ */ new Set([
|
|
104994
105843
|
SUBMIT_UPDATE,
|
|
105844
|
+
SUBMIT_NONINCLUDED,
|
|
104995
105845
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
104996
105846
|
VALIDATION_ACK
|
|
104997
105847
|
]);
|
|
@@ -104999,7 +105849,9 @@ a=end-of-candidates
|
|
|
104999
105849
|
AUTHORIZATION_REQUEST,
|
|
105000
105850
|
NONCE_CONTRIBUTION,
|
|
105001
105851
|
AGGREGATED_NONCE,
|
|
105002
|
-
SIGNATURE_AUTHORIZATION
|
|
105852
|
+
SIGNATURE_AUTHORIZATION,
|
|
105853
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
105854
|
+
FALLBACK_SIGNATURE
|
|
105003
105855
|
]);
|
|
105004
105856
|
function isAggregationMessageType(type) {
|
|
105005
105857
|
return KEYGEN_VALUES.has(type) || UPDATE_VALUES.has(type) || SIGN_VALUES.has(type);
|
|
@@ -111160,7 +112012,7 @@ ${value2}`;
|
|
|
111160
112012
|
* backfill historical events to late subscribers: republishing gives late
|
|
111161
112013
|
* joiners a window to discover the message without requiring protocol
|
|
111162
112014
|
* changes. Relay rate-limit / publish failures inside the interval are
|
|
111163
|
-
* caught and logged rather than propagated
|
|
112015
|
+
* caught and logged rather than propagated - the caller should stop the
|
|
111164
112016
|
* repeater once the protocol condition is satisfied.
|
|
111165
112017
|
*/
|
|
111166
112018
|
publishRepeating(message2, sender, intervalMs, recipient) {
|
|
@@ -111608,6 +112460,30 @@ ${value2}`;
|
|
|
111608
112460
|
|
|
111609
112461
|
// src/core/aggregation/runner/service-runner.ts
|
|
111610
112462
|
init_shim();
|
|
112463
|
+
|
|
112464
|
+
// src/core/beacon/fee-estimator.ts
|
|
112465
|
+
init_shim();
|
|
112466
|
+
var StaticFeeEstimator = class {
|
|
112467
|
+
satsPerVbyte;
|
|
112468
|
+
/**
|
|
112469
|
+
* @param satsPerVbyte Fee rate in satoshis per virtual byte. Default: 5 sat/vB.
|
|
112470
|
+
*/
|
|
112471
|
+
constructor(satsPerVbyte = 5) {
|
|
112472
|
+
if (satsPerVbyte < 0 || !Number.isFinite(satsPerVbyte)) {
|
|
112473
|
+
throw new Error(`Invalid satsPerVbyte: ${satsPerVbyte}`);
|
|
112474
|
+
}
|
|
112475
|
+
this.satsPerVbyte = satsPerVbyte;
|
|
112476
|
+
}
|
|
112477
|
+
async estimateFee(vsize) {
|
|
112478
|
+
if (vsize < 0 || !Number.isFinite(vsize)) {
|
|
112479
|
+
throw new Error(`Invalid vsize: ${vsize}`);
|
|
112480
|
+
}
|
|
112481
|
+
return BigInt(Math.ceil(vsize * this.satsPerVbyte));
|
|
112482
|
+
}
|
|
112483
|
+
};
|
|
112484
|
+
var DEFAULT_FEE_ESTIMATOR = new StaticFeeEstimator(5);
|
|
112485
|
+
|
|
112486
|
+
// src/core/aggregation/runner/service-runner.ts
|
|
111611
112487
|
var DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 6e4;
|
|
111612
112488
|
var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEventEmitter {
|
|
111613
112489
|
/** Direct access to the underlying state machine for advanced use. */
|
|
@@ -111618,9 +112494,17 @@ ${value2}`;
|
|
|
111618
112494
|
#onOptInReceived;
|
|
111619
112495
|
#onReadyToFinalize;
|
|
111620
112496
|
#onProvideTxData;
|
|
112497
|
+
#feeEstimator;
|
|
111621
112498
|
#cohortTtlMs;
|
|
111622
112499
|
#phaseTimeoutMs;
|
|
111623
112500
|
#advertRepeatIntervalMs;
|
|
112501
|
+
#autoFallbackOnStall;
|
|
112502
|
+
/** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
|
|
112503
|
+
static #SIGNING_PHASES = [
|
|
112504
|
+
"SigningStarted" /* SigningStarted */,
|
|
112505
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
112506
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
112507
|
+
];
|
|
111624
112508
|
/** Per-cohort run state, keyed by cohortId. */
|
|
111625
112509
|
#contexts = /* @__PURE__ */ new Map();
|
|
111626
112510
|
#handlersRegistered = false;
|
|
@@ -111635,9 +112519,11 @@ ${value2}`;
|
|
|
111635
112519
|
finalize: acceptedCount >= minRequired
|
|
111636
112520
|
}));
|
|
111637
112521
|
this.#onProvideTxData = options2.onProvideTxData;
|
|
112522
|
+
this.#feeEstimator = options2.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
111638
112523
|
this.#cohortTtlMs = options2.cohortTtlMs;
|
|
111639
112524
|
this.#phaseTimeoutMs = options2.phaseTimeoutMs;
|
|
111640
112525
|
this.#advertRepeatIntervalMs = options2.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
112526
|
+
this.#autoFallbackOnStall = options2.autoFallbackOnStall ?? false;
|
|
111641
112527
|
this.session = new AggregationService({
|
|
111642
112528
|
// The coordinator never signs, so the state machine receives only the
|
|
111643
112529
|
// public half of the operator's keypair (see ADR 038). The full keypair
|
|
@@ -111799,11 +112685,42 @@ ${value2}`;
|
|
|
111799
112685
|
ctx.phaseTimer = void 0;
|
|
111800
112686
|
if (this.#phaseTimeoutMs === void 0) return;
|
|
111801
112687
|
ctx.phaseTimer = setTimeout(() => {
|
|
112688
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
112689
|
+
const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
|
|
112690
|
+
if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
|
|
112691
|
+
this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
|
|
112692
|
+
return;
|
|
112693
|
+
}
|
|
111802
112694
|
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
111803
112695
|
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
111804
112696
|
this.#failCohort(ctx, new Error(reason));
|
|
111805
112697
|
}, this.#phaseTimeoutMs);
|
|
111806
112698
|
}
|
|
112699
|
+
/**
|
|
112700
|
+
* Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
|
|
112701
|
+
* fallback (script-path) signatures instead (ADR 042). Idempotent and safe
|
|
112702
|
+
* against the optimistic completion: it commits the cohort to the fallback
|
|
112703
|
+
* path synchronously (the `committedPath` latch) before sending anything, so a
|
|
112704
|
+
* late optimistic signature can no longer complete-and-broadcast a competing
|
|
112705
|
+
* spend of the same UTXO. No-op if the cohort is unknown, already settled, or
|
|
112706
|
+
* already committed to a path.
|
|
112707
|
+
*
|
|
112708
|
+
* Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
|
|
112709
|
+
* set; otherwise call it from an operator decision (a UI "fall back now"
|
|
112710
|
+
* action). Throws only if the underlying state machine rejects the transition
|
|
112711
|
+
* (e.g. signing has not started).
|
|
112712
|
+
*/
|
|
112713
|
+
async triggerFallback(cohortId) {
|
|
112714
|
+
const ctx = this.#contexts.get(cohortId);
|
|
112715
|
+
if (!ctx || ctx.settled || ctx.committedPath) return;
|
|
112716
|
+
const messages2 = this.session.startFallbackSigning(cohortId);
|
|
112717
|
+
ctx.committedPath = "fallback";
|
|
112718
|
+
this.#stopAdvertRepeating(ctx);
|
|
112719
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
112720
|
+
const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
|
|
112721
|
+
this.emit("fallback-started", { cohortId, sessionId });
|
|
112722
|
+
await this.#sendAll(messages2);
|
|
112723
|
+
}
|
|
111807
112724
|
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
111808
112725
|
#onPhaseMaybeChanged(ctx) {
|
|
111809
112726
|
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
@@ -111892,13 +112809,15 @@ ${value2}`;
|
|
|
111892
112809
|
static #HANDLED_MESSAGE_TYPES = [
|
|
111893
112810
|
COHORT_OPT_IN,
|
|
111894
112811
|
SUBMIT_UPDATE,
|
|
112812
|
+
SUBMIT_NONINCLUDED,
|
|
111895
112813
|
VALIDATION_ACK,
|
|
111896
112814
|
NONCE_CONTRIBUTION,
|
|
111897
|
-
SIGNATURE_AUTHORIZATION
|
|
112815
|
+
SIGNATURE_AUTHORIZATION,
|
|
112816
|
+
FALLBACK_SIGNATURE
|
|
111898
112817
|
];
|
|
111899
112818
|
/**
|
|
111900
112819
|
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
111901
|
-
* are DID-scoped and cohort-agnostic
|
|
112820
|
+
* are DID-scoped and cohort-agnostic - one registration serves every cohort
|
|
111902
112821
|
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
111903
112822
|
* handler via the inbound message's cohortId.
|
|
111904
112823
|
*/
|
|
@@ -111907,9 +112826,11 @@ ${value2}`;
|
|
|
111907
112826
|
this.#handlersRegistered = true;
|
|
111908
112827
|
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
|
|
111909
112828
|
this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
|
|
112829
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
|
|
111910
112830
|
this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
|
|
111911
112831
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
111912
112832
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
112833
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
|
|
111913
112834
|
}
|
|
111914
112835
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
111915
112836
|
#unregisterHandlers() {
|
|
@@ -112029,7 +112950,8 @@ ${value2}`;
|
|
|
112029
112950
|
const txData = await this.#onProvideTxData({
|
|
112030
112951
|
cohortId: ctx.cohortId,
|
|
112031
112952
|
beaconAddress: cohort.beaconAddress,
|
|
112032
|
-
signalBytes: cohort.signalBytes
|
|
112953
|
+
signalBytes: cohort.signalBytes,
|
|
112954
|
+
feeEstimator: this.#feeEstimator
|
|
112033
112955
|
});
|
|
112034
112956
|
const authMsgs = this.session.startSigning(ctx.cohortId, txData);
|
|
112035
112957
|
const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
|
|
@@ -112070,6 +112992,32 @@ ${value2}`;
|
|
|
112070
112992
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
112071
112993
|
*/
|
|
112072
112994
|
async #handleSignatureAuthorization(msg) {
|
|
112995
|
+
if (this.#stopped) return;
|
|
112996
|
+
const ctx = this.#contextFor(msg);
|
|
112997
|
+
if (!ctx) return;
|
|
112998
|
+
try {
|
|
112999
|
+
this.session.receive(msg);
|
|
113000
|
+
this.#drainRejections(ctx);
|
|
113001
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
113002
|
+
if (ctx.committedPath === "fallback") return;
|
|
113003
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
113004
|
+
if (result) {
|
|
113005
|
+
ctx.committedPath = "optimistic";
|
|
113006
|
+
this.#completeCohort(ctx, result);
|
|
113007
|
+
}
|
|
113008
|
+
} catch (err) {
|
|
113009
|
+
this.#failCohort(ctx, err);
|
|
113010
|
+
}
|
|
113011
|
+
}
|
|
113012
|
+
/**
|
|
113013
|
+
* Handler for receiving fallback (k-of-n script-path) signatures. The state
|
|
113014
|
+
* machine assembles and finalizes the fallback spend once k valid signatures
|
|
113015
|
+
* are in; the result is then emitted and the cohort's completion resolves. The
|
|
113016
|
+
* cohort is already committed to the fallback path (via {@link triggerFallback}).
|
|
113017
|
+
* @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
|
|
113018
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
113019
|
+
*/
|
|
113020
|
+
async #handleFallbackSignature(msg) {
|
|
112073
113021
|
if (this.#stopped) return;
|
|
112074
113022
|
const ctx = this.#contextFor(msg);
|
|
112075
113023
|
if (!ctx) return;
|
|
@@ -112143,7 +113091,8 @@ ${value2}`;
|
|
|
112143
113091
|
COHORT_READY,
|
|
112144
113092
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
112145
113093
|
AUTHORIZATION_REQUEST,
|
|
112146
|
-
AGGREGATED_NONCE
|
|
113094
|
+
AGGREGATED_NONCE,
|
|
113095
|
+
FALLBACK_AUTHORIZATION_REQUEST
|
|
112147
113096
|
];
|
|
112148
113097
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
112149
113098
|
#unregisterHandlers() {
|
|
@@ -112178,7 +113127,7 @@ ${value2}`;
|
|
|
112178
113127
|
*
|
|
112179
113128
|
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
112180
113129
|
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
112181
|
-
* `start()`, and listen for `cohort-complete`
|
|
113130
|
+
* `start()`, and listen for `cohort-complete` - the runner already drives
|
|
112182
113131
|
* any number of cohorts concurrently.
|
|
112183
113132
|
*
|
|
112184
113133
|
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
@@ -112213,6 +113162,7 @@ ${value2}`;
|
|
|
112213
113162
|
this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
|
|
112214
113163
|
this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
|
|
112215
113164
|
this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
|
|
113165
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
|
|
112216
113166
|
}
|
|
112217
113167
|
/**
|
|
112218
113168
|
* Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
|
|
@@ -112268,8 +113218,13 @@ ${value2}`;
|
|
|
112268
113218
|
cohortId,
|
|
112269
113219
|
beaconAddress: info.beaconAddress
|
|
112270
113220
|
});
|
|
112271
|
-
|
|
112272
|
-
|
|
113221
|
+
if (signedUpdate === null) {
|
|
113222
|
+
await this.#sendAll(this.session.declineUpdate(cohortId));
|
|
113223
|
+
this.emit("update-declined", { cohortId });
|
|
113224
|
+
} else {
|
|
113225
|
+
await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
|
|
113226
|
+
this.emit("update-submitted", { cohortId });
|
|
113227
|
+
}
|
|
112273
113228
|
} catch (err) {
|
|
112274
113229
|
this.emit("error", err);
|
|
112275
113230
|
}
|
|
@@ -112345,23 +113300,62 @@ ${value2}`;
|
|
|
112345
113300
|
const cohortId = msg.body?.cohortId;
|
|
112346
113301
|
if (!cohortId) return;
|
|
112347
113302
|
await this.#sendAll(this.session.generatePartialSignature(cohortId));
|
|
112348
|
-
|
|
112349
|
-
|
|
112350
|
-
|
|
112351
|
-
|
|
112352
|
-
|
|
112353
|
-
|
|
112354
|
-
|
|
112355
|
-
|
|
112356
|
-
|
|
112357
|
-
|
|
112358
|
-
|
|
112359
|
-
|
|
113303
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
113304
|
+
} catch (err) {
|
|
113305
|
+
this.emit("error", err);
|
|
113306
|
+
}
|
|
113307
|
+
}
|
|
113308
|
+
/**
|
|
113309
|
+
* Internal: handler for fallback authorization requests. The service abandoned
|
|
113310
|
+
* the optimistic key path; the member authorizes the k-of-n script-path spend
|
|
113311
|
+
* of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
|
|
113312
|
+
* the decision, signs the fallback, and completes once it has contributed (the
|
|
113313
|
+
* service needs only k members).
|
|
113314
|
+
* @param {BaseMessage} msg - The received fallback authorization request message.
|
|
113315
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
113316
|
+
*/
|
|
113317
|
+
async #handleFallbackAuthorizationRequest(msg) {
|
|
113318
|
+
if (this.#stopped) return;
|
|
113319
|
+
try {
|
|
113320
|
+
this.session.receive(msg);
|
|
113321
|
+
const cohortId = msg.body?.cohortId;
|
|
113322
|
+
if (!cohortId) return;
|
|
113323
|
+
const req = this.session.pendingFallbackRequests.get(cohortId);
|
|
113324
|
+
if (!req) return;
|
|
113325
|
+
this.emit("fallback-requested", req);
|
|
113326
|
+
const decision = await this.#onApproveSigning(req);
|
|
113327
|
+
if (!decision.approved) {
|
|
113328
|
+
this.emit("cohort-failed", { cohortId, reason: "Fallback signing rejected by participant" });
|
|
113329
|
+
return;
|
|
112360
113330
|
}
|
|
113331
|
+
await this.#sendAll(this.session.approveFallback(cohortId));
|
|
113332
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
112361
113333
|
} catch (err) {
|
|
112362
113334
|
this.emit("error", err);
|
|
112363
113335
|
}
|
|
112364
113336
|
}
|
|
113337
|
+
/**
|
|
113338
|
+
* Internal: emit `cohort-complete` with the participant's retained sidecar once
|
|
113339
|
+
* the cohort has reached the Complete phase. Surfaces the CAS Announcement map
|
|
113340
|
+
* (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
|
|
113341
|
+
* DID resolution. Read via getValidation (not pendingValidations, which lists
|
|
113342
|
+
* only the AwaitingValidation phase) so the sidecar is still available at
|
|
113343
|
+
* Complete. Shared by the optimistic and fallback completion paths.
|
|
113344
|
+
*/
|
|
113345
|
+
#emitCohortCompleteIfDone(cohortId) {
|
|
113346
|
+
if (this.session.getCohortPhase(cohortId) !== "Complete" /* Complete */) return;
|
|
113347
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
113348
|
+
if (!info) return;
|
|
113349
|
+
const validation = this.session.getValidation(cohortId);
|
|
113350
|
+
this.emit("cohort-complete", {
|
|
113351
|
+
cohortId,
|
|
113352
|
+
beaconAddress: info.beaconAddress,
|
|
113353
|
+
beaconType: validation?.beaconType ?? "",
|
|
113354
|
+
included: validation?.included ?? true,
|
|
113355
|
+
casAnnouncement: validation?.casAnnouncement,
|
|
113356
|
+
smtProof: validation?.smtProof
|
|
113357
|
+
});
|
|
113358
|
+
}
|
|
112365
113359
|
/**
|
|
112366
113360
|
* Internal: send helper to ensure messages are sent sequentially. This is important for protocol
|
|
112367
113361
|
* correctness, as some transports may not guarantee message order if sent in parallel.
|
|
@@ -112378,6 +113372,7 @@ ${value2}`;
|
|
|
112378
113372
|
|
|
112379
113373
|
// src/core/aggregation/runner/aggregation-runner.ts
|
|
112380
113374
|
init_shim();
|
|
113375
|
+
init_utils();
|
|
112381
113376
|
var AggregationRunner = class {
|
|
112382
113377
|
/**
|
|
112383
113378
|
* Run a cohort of ONE participant entirely in-process and return the
|
|
@@ -112385,8 +113380,8 @@ ${value2}`;
|
|
|
112385
113380
|
*
|
|
112386
113381
|
* One party plays both the coordinating service and the lone participant,
|
|
112387
113382
|
* connected over an {@link InMemoryTransport} (no relay or HTTP server). This
|
|
112388
|
-
* makes the single-participant aggregate-beacon path
|
|
112389
|
-
* two-axis beacon matrix
|
|
113383
|
+
* makes the single-participant aggregate-beacon path (the N=1 corner of the
|
|
113384
|
+
* two-axis beacon matrix, see ADR 037) first-class, useful for generating
|
|
112390
113385
|
* and reproducing single-participant aggregate test vectors.
|
|
112391
113386
|
*
|
|
112392
113387
|
* The service advertises a cohort with `minParticipants: 1`; the participant
|
|
@@ -112404,12 +113399,15 @@ ${value2}`;
|
|
|
112404
113399
|
transport.registerPeer(options2.participant.did, options2.participant.keys.publicKey.compressed);
|
|
112405
113400
|
transport.registerPeer(options2.service.did, options2.service.keys.publicKey.compressed);
|
|
112406
113401
|
transport.start();
|
|
113402
|
+
const recoveryKey = options2.config.recoveryKey ?? bytesToHex(options2.service.keys.publicKey.compressed.slice(1));
|
|
113403
|
+
const recoverySequence = options2.config.recoverySequence ?? DEFAULT_RECOVERY_SEQUENCE;
|
|
112407
113404
|
const service = new AggregationServiceRunner({
|
|
112408
113405
|
transport,
|
|
112409
113406
|
did: options2.service.did,
|
|
112410
113407
|
keys: options2.service.keys,
|
|
112411
|
-
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType },
|
|
113408
|
+
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType, recoveryKey, recoverySequence, fundingModel: DEFAULT_FUNDING_MODEL },
|
|
112412
113409
|
onProvideTxData: options2.onProvideTxData,
|
|
113410
|
+
feeEstimator: options2.feeEstimator,
|
|
112413
113411
|
cohortTtlMs: options2.cohortTtlMs,
|
|
112414
113412
|
phaseTimeoutMs: options2.phaseTimeoutMs,
|
|
112415
113413
|
// In-process bus with the participant already listening: a single advert
|
|
@@ -112465,29 +113463,7 @@ ${value2}`;
|
|
|
112465
113463
|
}
|
|
112466
113464
|
};
|
|
112467
113465
|
|
|
112468
|
-
// src/core/beacon/fee-estimator.ts
|
|
112469
|
-
init_shim();
|
|
112470
|
-
var StaticFeeEstimator = class {
|
|
112471
|
-
satsPerVbyte;
|
|
112472
|
-
/**
|
|
112473
|
-
* @param satsPerVbyte Fee rate in satoshis per virtual byte. Default: 5 sat/vB.
|
|
112474
|
-
*/
|
|
112475
|
-
constructor(satsPerVbyte = 5) {
|
|
112476
|
-
if (satsPerVbyte < 0 || !Number.isFinite(satsPerVbyte)) {
|
|
112477
|
-
throw new Error(`Invalid satsPerVbyte: ${satsPerVbyte}`);
|
|
112478
|
-
}
|
|
112479
|
-
this.satsPerVbyte = satsPerVbyte;
|
|
112480
|
-
}
|
|
112481
|
-
async estimateFee(vsize) {
|
|
112482
|
-
if (vsize < 0 || !Number.isFinite(vsize)) {
|
|
112483
|
-
throw new Error(`Invalid vsize: ${vsize}`);
|
|
112484
|
-
}
|
|
112485
|
-
return BigInt(Math.ceil(vsize * this.satsPerVbyte));
|
|
112486
|
-
}
|
|
112487
|
-
};
|
|
112488
|
-
|
|
112489
113466
|
// src/core/beacon/beacon.ts
|
|
112490
|
-
var DEFAULT_FEE_ESTIMATOR = new StaticFeeEstimator(5);
|
|
112491
113467
|
var P2TR_BEACON_TX_VSIZE = 160;
|
|
112492
113468
|
var P2WPKH_BEACON_TX_VSIZE = 155;
|
|
112493
113469
|
var P2PKH_BEACON_TX_VSIZE = 240;
|
|
@@ -112496,6 +113472,20 @@ ${value2}`;
|
|
|
112496
113472
|
p2wpkh: P2WPKH_BEACON_TX_VSIZE,
|
|
112497
113473
|
p2tr: P2TR_BEACON_TX_VSIZE
|
|
112498
113474
|
};
|
|
113475
|
+
var CHANGE_OUTPUT_VBYTES = {
|
|
113476
|
+
p2pkh: 34,
|
|
113477
|
+
p2wpkh: 31,
|
|
113478
|
+
p2tr: 43
|
|
113479
|
+
};
|
|
113480
|
+
var DUST_LIMIT_SATS2 = {
|
|
113481
|
+
p2pkh: 546,
|
|
113482
|
+
p2wpkh: 294,
|
|
113483
|
+
p2tr: 330
|
|
113484
|
+
};
|
|
113485
|
+
function beaconTxVsize(beaconKind, changeKind) {
|
|
113486
|
+
const base5 = SINGLETON_BEACON_TX_VSIZE[beaconKind] - CHANGE_OUTPUT_VBYTES[beaconKind];
|
|
113487
|
+
return base5 + CHANGE_OUTPUT_VBYTES[changeKind];
|
|
113488
|
+
}
|
|
112499
113489
|
function detectSingletonScriptKind(bitcoinAddress, network) {
|
|
112500
113490
|
const decoded = Address(network).decode(bitcoinAddress);
|
|
112501
113491
|
if (decoded.type === "pkh") return "p2pkh";
|
|
@@ -112512,6 +113502,26 @@ ${value2}`;
|
|
|
112512
113502
|
if (kind === "p2wpkh") return p2wpkh(pubkey, network).address;
|
|
112513
113503
|
return p2tr(pubkey.slice(1, 33), void 0, network).address;
|
|
112514
113504
|
}
|
|
113505
|
+
function resolveChangeAddress(beaconAddress, network, changeAddress) {
|
|
113506
|
+
if (!changeAddress || changeAddress === beaconAddress) return beaconAddress;
|
|
113507
|
+
try {
|
|
113508
|
+
Address(network).decode(changeAddress);
|
|
113509
|
+
} catch {
|
|
113510
|
+
throw new BeaconError(
|
|
113511
|
+
`Invalid change address "${changeAddress}" for network "${network}".`,
|
|
113512
|
+
"INVALID_CHANGE_ADDRESS",
|
|
113513
|
+
{ changeAddress, network }
|
|
113514
|
+
);
|
|
113515
|
+
}
|
|
113516
|
+
return changeAddress;
|
|
113517
|
+
}
|
|
113518
|
+
function changeOutputKind(changeAddress, network) {
|
|
113519
|
+
try {
|
|
113520
|
+
return detectSingletonScriptKind(changeAddress, network);
|
|
113521
|
+
} catch {
|
|
113522
|
+
return "p2tr";
|
|
113523
|
+
}
|
|
113524
|
+
}
|
|
112515
113525
|
function opReturnScript(signalBytes) {
|
|
112516
113526
|
return Script.encode(["RETURN", signalBytes]);
|
|
112517
113527
|
}
|
|
@@ -112538,9 +113548,10 @@ ${value2}`;
|
|
|
112538
113548
|
async function buildAggregationBeaconTx(opts) {
|
|
112539
113549
|
const feeEstimator = opts.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
112540
113550
|
const { utxo, prevTxBytes } = await fetchSpendableUtxo(opts.beaconAddress, opts.bitcoin);
|
|
112541
|
-
const
|
|
112542
|
-
const witnessScript =
|
|
112543
|
-
const
|
|
113551
|
+
const changeAddress = resolveChangeAddress(opts.beaconAddress, opts.network, opts.changeAddress);
|
|
113552
|
+
const witnessScript = OutScript.encode(Address(opts.network).decode(opts.beaconAddress));
|
|
113553
|
+
const changeKind = changeOutputKind(changeAddress, opts.network);
|
|
113554
|
+
const feeSats = await feeEstimator.estimateFee(beaconTxVsize("p2tr", changeKind));
|
|
112544
113555
|
if (BigInt(utxo.value) <= feeSats) {
|
|
112545
113556
|
throw new BeaconError(
|
|
112546
113557
|
`UTXO value (${utxo.value}) insufficient to cover fee (${feeSats}).`,
|
|
@@ -112556,13 +113567,17 @@ ${value2}`;
|
|
|
112556
113567
|
witnessUtxo: { amount: BigInt(utxo.value), script: witnessScript },
|
|
112557
113568
|
tapInternalKey: opts.internalPubkey
|
|
112558
113569
|
});
|
|
112559
|
-
|
|
113570
|
+
const changeValue = BigInt(utxo.value) - feeSats;
|
|
113571
|
+
if (changeValue >= BigInt(DUST_LIMIT_SATS2[changeKind])) {
|
|
113572
|
+
tx.addOutputAddress(changeAddress, changeValue, opts.network);
|
|
113573
|
+
}
|
|
112560
113574
|
tx.addOutput({ script: opReturnScript(opts.signalBytes), amount: 0n });
|
|
112561
113575
|
return {
|
|
112562
113576
|
tx,
|
|
112563
113577
|
prevOutScripts: [witnessScript],
|
|
112564
113578
|
prevOutValues: [BigInt(utxo.value)],
|
|
112565
113579
|
beaconAddress: opts.beaconAddress,
|
|
113580
|
+
changeAddress,
|
|
112566
113581
|
utxo,
|
|
112567
113582
|
feeSats,
|
|
112568
113583
|
scriptKind: "p2tr"
|
|
@@ -112618,7 +113633,7 @@ ${value2}`;
|
|
|
112618
113633
|
*
|
|
112619
113634
|
* Composed from the three extracted phases ({@link buildSinglePartyTx},
|
|
112620
113635
|
* {@link signSinglePartyTx}, {@link broadcastRawTx}) so each piece can be exercised
|
|
112621
|
-
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead
|
|
113636
|
+
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead:
|
|
112622
113637
|
* the multi-party path can't share the signing phase, but the tx-construction
|
|
112623
113638
|
* plumbing (UTXO fetch + OP_RETURN output + change output) is shared.
|
|
112624
113639
|
*
|
|
@@ -112640,7 +113655,8 @@ ${value2}`;
|
|
|
112640
113655
|
prevTxBytes,
|
|
112641
113656
|
signer,
|
|
112642
113657
|
bitcoin,
|
|
112643
|
-
feeEstimator
|
|
113658
|
+
feeEstimator,
|
|
113659
|
+
changeAddress: options2?.changeAddress
|
|
112644
113660
|
});
|
|
112645
113661
|
const signedHex = await this.signSinglePartyTx(plan, signer);
|
|
112646
113662
|
return this.broadcastRawTx(bitcoin, signedHex);
|
|
@@ -112650,15 +113666,17 @@ ${value2}`;
|
|
|
112650
113666
|
*
|
|
112651
113667
|
* Detects the beacon address script kind (P2PKH / P2WPKH / P2TR) and configures
|
|
112652
113668
|
* the input accordingly. Validates that the signer's pubkey produces the beacon
|
|
112653
|
-
* address under that script kind
|
|
113669
|
+
* address under that script kind: without this check, a misconfigured caller
|
|
112654
113670
|
* would burn a real UTXO on a tx that fails at broadcast. Fees are computed from
|
|
112655
|
-
* the per-kind {@link SINGLETON_BEACON_TX_VSIZE} constant
|
|
112656
|
-
* round-trip
|
|
113671
|
+
* the per-kind {@link SINGLETON_BEACON_TX_VSIZE} constant (via {@link beaconTxVsize}),
|
|
113672
|
+
* avoiding any probe-sign round-trip; a change address of a different kind re-sizes
|
|
113673
|
+
* the fee by the change output's size delta so it stays a valid upper bound.
|
|
112657
113674
|
*/
|
|
112658
113675
|
async buildSinglePartyTx(opts) {
|
|
112659
113676
|
const network = opts.bitcoin.data;
|
|
112660
113677
|
const pubkey = opts.signer.publicKey;
|
|
112661
113678
|
const kind = detectSingletonScriptKind(opts.beaconAddress, network);
|
|
113679
|
+
const changeAddress = resolveChangeAddress(opts.beaconAddress, network, opts.changeAddress);
|
|
112662
113680
|
const derivedAddress = deriveSingletonAddress(kind, pubkey, network);
|
|
112663
113681
|
if (derivedAddress !== opts.beaconAddress) {
|
|
112664
113682
|
throw new BeaconError(
|
|
@@ -112667,7 +113685,8 @@ ${value2}`;
|
|
|
112667
113685
|
{ kind, address: opts.beaconAddress, derivedAddress }
|
|
112668
113686
|
);
|
|
112669
113687
|
}
|
|
112670
|
-
const
|
|
113688
|
+
const changeKind = changeOutputKind(changeAddress, network);
|
|
113689
|
+
const feeSats = await opts.feeEstimator.estimateFee(beaconTxVsize(kind, changeKind));
|
|
112671
113690
|
const amount = BigInt(opts.utxo.value);
|
|
112672
113691
|
if (amount <= feeSats) {
|
|
112673
113692
|
throw new BeaconError(
|
|
@@ -112704,13 +113723,17 @@ ${value2}`;
|
|
|
112704
113723
|
tapInternalKey: internalKey
|
|
112705
113724
|
});
|
|
112706
113725
|
}
|
|
112707
|
-
|
|
113726
|
+
const changeValue = amount - feeSats;
|
|
113727
|
+
if (changeValue >= BigInt(DUST_LIMIT_SATS2[changeKind])) {
|
|
113728
|
+
tx.addOutputAddress(changeAddress, changeValue, network);
|
|
113729
|
+
}
|
|
112708
113730
|
tx.addOutput({ script: opReturnScript(opts.signalBytes), amount: 0n });
|
|
112709
113731
|
return {
|
|
112710
113732
|
tx,
|
|
112711
113733
|
prevOutScripts: [prevOutScript],
|
|
112712
113734
|
prevOutValues: [amount],
|
|
112713
113735
|
beaconAddress: opts.beaconAddress,
|
|
113736
|
+
changeAddress,
|
|
112714
113737
|
utxo: opts.utxo,
|
|
112715
113738
|
feeSats,
|
|
112716
113739
|
scriptKind: kind
|
|
@@ -128537,7 +129560,7 @@ ${value2}`;
|
|
|
128537
129560
|
#verificationMethod;
|
|
128538
129561
|
#beaconService;
|
|
128539
129562
|
/**
|
|
128540
|
-
* @internal
|
|
129563
|
+
* @internal Use {@link DidBtcr2.update} to create instances.
|
|
128541
129564
|
*/
|
|
128542
129565
|
constructor(params) {
|
|
128543
129566
|
this.#sourceDocument = params.sourceDocument;
|
|
@@ -128574,7 +129597,7 @@ ${value2}`;
|
|
|
128574
129597
|
const targetDocument = JSONPatch.apply(sourceDocument, patches);
|
|
128575
129598
|
if (targetDocument.id !== sourceDocument.id) {
|
|
128576
129599
|
throw new UpdateError(
|
|
128577
|
-
`Patches must not change the DID document id (source "${sourceDocument.id}"
|
|
129600
|
+
`Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
|
|
128578
129601
|
INVALID_DID_UPDATE,
|
|
128579
129602
|
{ sourceId: sourceDocument.id, targetId: targetDocument.id }
|
|
128580
129603
|
);
|
|
@@ -128658,8 +129681,8 @@ ${value2}`;
|
|
|
128658
129681
|
}
|
|
128659
129682
|
/**
|
|
128660
129683
|
* Advance the state machine. Returns either:
|
|
128661
|
-
* - `{ status: 'action-required', needs }`
|
|
128662
|
-
* - `{ status: 'complete', result }`
|
|
129684
|
+
* - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
|
|
129685
|
+
* - `{ status: 'complete', result }` update is signed and broadcast
|
|
128663
129686
|
*/
|
|
128664
129687
|
advance() {
|
|
128665
129688
|
while (true) {
|
|
@@ -128672,7 +129695,7 @@ ${value2}`;
|
|
|
128672
129695
|
continue;
|
|
128673
129696
|
}
|
|
128674
129697
|
// Phase: Sign
|
|
128675
|
-
// Emit NeedSigningKey
|
|
129698
|
+
// Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
|
|
128676
129699
|
case "Sign": {
|
|
128677
129700
|
return {
|
|
128678
129701
|
status: "action-required",
|
|
@@ -128855,8 +129878,8 @@ ${value2}`;
|
|
|
128855
129878
|
*
|
|
128856
129879
|
* Factory method that validates the update parameters and returns a sans-I/O
|
|
128857
129880
|
* {@link Updater} state machine. The caller drives the updater through its
|
|
128858
|
-
* phases (Construct
|
|
128859
|
-
* `provide()`. The method package performs **zero I/O
|
|
129881
|
+
* phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
|
|
129882
|
+
* `provide()`. The method package performs **zero I/O**: signing key retrieval
|
|
128860
129883
|
* (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
|
|
128861
129884
|
*
|
|
128862
129885
|
* For a fully-wired version with Bitcoin broadcast and key handling, see
|
|
@@ -128974,7 +129997,7 @@ ${value2}`;
|
|
|
128974
129997
|
#unsortedUpdates = [];
|
|
128975
129998
|
#resolvedResponse = null;
|
|
128976
129999
|
/**
|
|
128977
|
-
* @internal
|
|
130000
|
+
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
128978
130001
|
*/
|
|
128979
130002
|
constructor(didComponents, sidecarData, currentDocument, options2) {
|
|
128980
130003
|
this.#didComponents = didComponents;
|
|
@@ -129210,8 +130233,8 @@ ${value2}`;
|
|
|
129210
130233
|
}
|
|
129211
130234
|
/**
|
|
129212
130235
|
* Advance the state machine. Returns either:
|
|
129213
|
-
* - `{ status: 'action-required', needs }`
|
|
129214
|
-
* - `{ status: 'resolved', result }`
|
|
130236
|
+
* - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
|
|
130237
|
+
* - `{ status: 'resolved', result }` - resolution complete
|
|
129215
130238
|
*
|
|
129216
130239
|
* Analogous to Rust's `Resolver::resolve()`.
|
|
129217
130240
|
*/
|