@did-btcr2/aggregation 0.1.0 → 0.3.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 +18107 -18096
- package/dist/browser.mjs +18107 -18096
- package/dist/cjs/core/index.js +2423 -0
- package/dist/cjs/index.js +3513 -3521
- package/dist/cjs/participant/index.js +2425 -0
- package/dist/cjs/service/index.js +3199 -0
- package/dist/esm/{runner/aggregation-runner.js → aggregation-runner.js} +4 -4
- package/dist/esm/aggregation-runner.js.map +1 -0
- package/dist/esm/core/beacon-strategy.js.map +1 -0
- package/dist/esm/core/cohort.js.map +1 -0
- package/dist/esm/core/conditions.js.map +1 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/fallback-spend.js.map +1 -0
- package/dist/esm/core/index.js +23 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/logger.js.map +1 -0
- package/dist/esm/core/messages/base.js.map +1 -0
- package/dist/esm/core/messages/bodies.js.map +1 -0
- package/dist/esm/core/messages/constants.js.map +1 -0
- package/dist/esm/core/messages/factories.js.map +1 -0
- package/dist/esm/core/messages/guards.js.map +1 -0
- package/dist/esm/core/messages/index.js.map +1 -0
- package/dist/esm/core/phases.js.map +1 -0
- package/dist/esm/core/recovery-policy.js.map +1 -0
- package/dist/esm/core/recovery-spend.js.map +1 -0
- package/dist/esm/core/signer.js.map +1 -0
- package/dist/esm/core/signing-session.js.map +1 -0
- package/dist/esm/core/transport/didcomm.js.map +1 -0
- package/dist/esm/core/transport/error.js.map +1 -0
- package/dist/esm/core/transport/http/envelope.js.map +1 -0
- package/dist/esm/core/transport/http/errors.js.map +1 -0
- package/dist/esm/core/transport/http/index.js +9 -0
- package/dist/esm/core/transport/http/index.js.map +1 -0
- package/dist/esm/core/transport/http/protocol.js.map +1 -0
- package/dist/esm/core/transport/http/request-auth.js.map +1 -0
- package/dist/esm/core/transport/in-memory.js.map +1 -0
- package/dist/esm/core/transport/index.js +12 -0
- package/dist/esm/core/transport/index.js.map +1 -0
- package/dist/esm/core/transport/nostr.js.map +1 -0
- package/dist/esm/core/transport/transport.js.map +1 -0
- package/dist/esm/core/typed-emitter.js.map +1 -0
- package/dist/esm/index.js +13 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/participant/events.js.map +1 -0
- package/dist/esm/{transport/http/client.js → participant/http-client.js} +6 -6
- package/dist/esm/participant/http-client.js.map +1 -0
- package/dist/esm/participant/index.js +13 -0
- package/dist/esm/participant/index.js.map +1 -0
- package/dist/esm/{runner → participant}/participant-runner.js +5 -5
- package/dist/esm/participant/participant-runner.js.map +1 -0
- package/dist/esm/{participant.js → participant/participant.js} +11 -11
- package/dist/esm/participant/participant.js.map +1 -0
- package/dist/esm/participant/sse-stream.js.map +1 -0
- package/dist/esm/service/events.js +2 -0
- package/dist/esm/{runner → service}/events.js.map +1 -1
- package/dist/esm/{transport/http/server.js → service/http-server.js} +6 -6
- package/dist/esm/service/http-server.js.map +1 -0
- package/dist/esm/service/inbox-buffer.js.map +1 -0
- package/dist/esm/service/index.js +16 -0
- package/dist/esm/service/index.js.map +1 -0
- package/dist/esm/service/nonce-cache.js.map +1 -0
- package/dist/esm/service/rate-limiter.js.map +1 -0
- package/dist/esm/{runner → service}/service-runner.js +5 -5
- package/dist/esm/service/service-runner.js.map +1 -0
- package/dist/esm/{service.js → service/service.js} +12 -12
- package/dist/esm/service/service.js.map +1 -0
- package/dist/esm/service/sse-writer.js.map +1 -0
- package/dist/esm/{transport/factory.js → transport-factory.js} +5 -5
- package/dist/esm/transport-factory.js.map +1 -0
- package/dist/types/{runner/aggregation-runner.d.ts → aggregation-runner.d.ts} +3 -3
- package/dist/types/aggregation-runner.d.ts.map +1 -0
- package/dist/types/{beacon-strategy.d.ts → core/beacon-strategy.d.ts} +2 -2
- package/dist/types/core/beacon-strategy.d.ts.map +1 -0
- package/dist/types/{cohort.d.ts → core/cohort.d.ts} +3 -3
- package/dist/types/core/cohort.d.ts.map +1 -0
- package/dist/types/core/conditions.d.ts.map +1 -0
- package/dist/types/core/errors.d.ts.map +1 -0
- package/dist/types/core/fallback-spend.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +15 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/logger.d.ts.map +1 -0
- package/dist/types/core/messages/base.d.ts.map +1 -0
- package/dist/types/core/messages/bodies.d.ts.map +1 -0
- package/dist/types/core/messages/constants.d.ts.map +1 -0
- package/dist/types/core/messages/factories.d.ts.map +1 -0
- package/dist/types/core/messages/guards.d.ts.map +1 -0
- package/dist/types/core/messages/index.d.ts.map +1 -0
- package/dist/types/core/phases.d.ts.map +1 -0
- package/dist/types/core/recovery-policy.d.ts.map +1 -0
- package/dist/types/core/recovery-spend.d.ts.map +1 -0
- package/dist/types/core/signer.d.ts.map +1 -0
- package/dist/types/core/signing-session.d.ts.map +1 -0
- package/dist/types/core/transport/didcomm.d.ts.map +1 -0
- package/dist/types/core/transport/error.d.ts.map +1 -0
- package/dist/types/core/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/core/transport/http/errors.d.ts.map +1 -0
- package/dist/types/core/transport/http/index.d.ts +5 -0
- package/dist/types/core/transport/http/index.d.ts.map +1 -0
- package/dist/types/core/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/core/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/core/transport/in-memory.d.ts.map +1 -0
- package/{src/transport/index.ts → dist/types/core/transport/index.d.ts} +1 -1
- package/dist/types/core/transport/index.d.ts.map +1 -0
- package/dist/types/core/transport/nostr.d.ts.map +1 -0
- package/dist/types/core/transport/transport.d.ts.map +1 -0
- package/dist/types/core/typed-emitter.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -16
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/{runner → participant}/events.d.ts +1 -76
- package/dist/types/participant/events.d.ts.map +1 -0
- package/dist/types/{transport/http/client.d.ts → participant/http-client.d.ts} +4 -4
- package/dist/types/participant/http-client.d.ts.map +1 -0
- package/dist/types/participant/index.d.ts +6 -0
- package/dist/types/participant/index.d.ts.map +1 -0
- package/dist/types/{runner → participant}/participant-runner.d.ts +6 -6
- package/dist/types/participant/participant-runner.d.ts.map +1 -0
- package/dist/types/{participant.d.ts → participant/participant.d.ts} +6 -6
- package/dist/types/participant/participant.d.ts.map +1 -0
- package/dist/types/participant/sse-stream.d.ts.map +1 -0
- package/dist/types/service/events.d.ts +76 -0
- package/dist/types/service/events.d.ts.map +1 -0
- package/dist/types/{transport/http/server.d.ts → service/http-server.d.ts} +4 -4
- package/dist/types/{transport/http/server.d.ts.map → service/http-server.d.ts.map} +1 -1
- package/dist/types/service/inbox-buffer.d.ts.map +1 -0
- package/dist/types/service/index.d.ts +9 -0
- package/dist/types/service/index.d.ts.map +1 -0
- package/dist/types/service/nonce-cache.d.ts.map +1 -0
- package/dist/types/service/rate-limiter.d.ts.map +1 -0
- package/dist/types/{runner → service}/service-runner.d.ts +4 -4
- package/dist/types/service/service-runner.d.ts.map +1 -0
- package/dist/types/{service.d.ts → service/service.d.ts} +6 -6
- package/dist/types/service/service.d.ts.map +1 -0
- package/dist/types/service/sse-writer.d.ts.map +1 -0
- package/dist/types/{transport/factory.d.ts → transport-factory.d.ts} +5 -5
- package/dist/types/transport-factory.d.ts.map +1 -0
- package/package.json +23 -6
- package/src/{runner/aggregation-runner.ts → aggregation-runner.ts} +7 -7
- package/src/{beacon-strategy.ts → core/beacon-strategy.ts} +2 -2
- package/src/{cohort.ts → core/cohort.ts} +3 -3
- package/src/core/index.ts +22 -0
- package/src/core/transport/http/index.ts +8 -0
- package/src/core/transport/index.ts +11 -0
- package/src/index.ts +13 -23
- package/src/{runner → participant}/events.ts +1 -59
- package/src/{transport/http/client.ts → participant/http-client.ts} +8 -8
- package/src/participant/index.ts +12 -0
- package/src/{runner → participant}/participant-runner.ts +10 -10
- package/src/{participant.ts → participant/participant.ts} +18 -18
- package/src/service/events.ts +58 -0
- package/src/{transport/http/server.ts → service/http-server.ts} +8 -8
- package/src/service/index.ts +15 -0
- package/src/{runner → service}/service-runner.ts +9 -9
- package/src/{service.ts → service/service.ts} +19 -19
- package/src/{transport/factory.ts → transport-factory.ts} +8 -8
- package/dist/esm/beacon-strategy.js.map +0 -1
- package/dist/esm/cohort.js.map +0 -1
- package/dist/esm/conditions.js.map +0 -1
- package/dist/esm/errors.js.map +0 -1
- package/dist/esm/fallback-spend.js.map +0 -1
- package/dist/esm/logger.js.map +0 -1
- package/dist/esm/messages/base.js.map +0 -1
- package/dist/esm/messages/bodies.js.map +0 -1
- package/dist/esm/messages/constants.js.map +0 -1
- package/dist/esm/messages/factories.js.map +0 -1
- package/dist/esm/messages/guards.js.map +0 -1
- package/dist/esm/messages/index.js.map +0 -1
- package/dist/esm/participant.js.map +0 -1
- package/dist/esm/phases.js.map +0 -1
- package/dist/esm/recovery-policy.js.map +0 -1
- package/dist/esm/recovery-spend.js.map +0 -1
- package/dist/esm/runner/aggregation-runner.js.map +0 -1
- package/dist/esm/runner/index.js +0 -6
- package/dist/esm/runner/index.js.map +0 -1
- package/dist/esm/runner/participant-runner.js.map +0 -1
- package/dist/esm/runner/service-runner.js.map +0 -1
- package/dist/esm/runner/typed-emitter.js.map +0 -1
- package/dist/esm/service.js.map +0 -1
- package/dist/esm/signer.js.map +0 -1
- package/dist/esm/signing-session.js.map +0 -1
- package/dist/esm/transport/didcomm.js.map +0 -1
- package/dist/esm/transport/error.js.map +0 -1
- package/dist/esm/transport/factory.js.map +0 -1
- package/dist/esm/transport/http/client.js.map +0 -1
- package/dist/esm/transport/http/envelope.js.map +0 -1
- package/dist/esm/transport/http/errors.js.map +0 -1
- package/dist/esm/transport/http/inbox-buffer.js.map +0 -1
- package/dist/esm/transport/http/index.js +0 -12
- package/dist/esm/transport/http/index.js.map +0 -1
- package/dist/esm/transport/http/nonce-cache.js.map +0 -1
- package/dist/esm/transport/http/protocol.js.map +0 -1
- package/dist/esm/transport/http/rate-limiter.js.map +0 -1
- package/dist/esm/transport/http/request-auth.js.map +0 -1
- package/dist/esm/transport/http/server.js.map +0 -1
- package/dist/esm/transport/http/sse-stream.js.map +0 -1
- package/dist/esm/transport/http/sse-writer.js.map +0 -1
- package/dist/esm/transport/in-memory.js.map +0 -1
- package/dist/esm/transport/index.js +0 -8
- package/dist/esm/transport/index.js.map +0 -1
- package/dist/esm/transport/nostr.js.map +0 -1
- package/dist/esm/transport/transport.js.map +0 -1
- package/dist/types/beacon-strategy.d.ts.map +0 -1
- package/dist/types/cohort.d.ts.map +0 -1
- package/dist/types/conditions.d.ts.map +0 -1
- package/dist/types/errors.d.ts.map +0 -1
- package/dist/types/fallback-spend.d.ts.map +0 -1
- package/dist/types/logger.d.ts.map +0 -1
- package/dist/types/messages/base.d.ts.map +0 -1
- package/dist/types/messages/bodies.d.ts.map +0 -1
- package/dist/types/messages/constants.d.ts.map +0 -1
- package/dist/types/messages/factories.d.ts.map +0 -1
- package/dist/types/messages/guards.d.ts.map +0 -1
- package/dist/types/messages/index.d.ts.map +0 -1
- package/dist/types/participant.d.ts.map +0 -1
- package/dist/types/phases.d.ts.map +0 -1
- package/dist/types/recovery-policy.d.ts.map +0 -1
- package/dist/types/recovery-spend.d.ts.map +0 -1
- package/dist/types/runner/aggregation-runner.d.ts.map +0 -1
- package/dist/types/runner/events.d.ts.map +0 -1
- package/dist/types/runner/index.d.ts +0 -6
- package/dist/types/runner/index.d.ts.map +0 -1
- package/dist/types/runner/participant-runner.d.ts.map +0 -1
- package/dist/types/runner/service-runner.d.ts.map +0 -1
- package/dist/types/runner/typed-emitter.d.ts.map +0 -1
- package/dist/types/service.d.ts.map +0 -1
- package/dist/types/signer.d.ts.map +0 -1
- package/dist/types/signing-session.d.ts.map +0 -1
- package/dist/types/transport/didcomm.d.ts.map +0 -1
- package/dist/types/transport/error.d.ts.map +0 -1
- package/dist/types/transport/factory.d.ts.map +0 -1
- package/dist/types/transport/http/client.d.ts.map +0 -1
- package/dist/types/transport/http/envelope.d.ts.map +0 -1
- package/dist/types/transport/http/errors.d.ts.map +0 -1
- package/dist/types/transport/http/inbox-buffer.d.ts.map +0 -1
- package/dist/types/transport/http/index.d.ts +0 -12
- package/dist/types/transport/http/index.d.ts.map +0 -1
- package/dist/types/transport/http/nonce-cache.d.ts.map +0 -1
- package/dist/types/transport/http/protocol.d.ts.map +0 -1
- package/dist/types/transport/http/rate-limiter.d.ts.map +0 -1
- package/dist/types/transport/http/request-auth.d.ts.map +0 -1
- package/dist/types/transport/http/sse-stream.d.ts.map +0 -1
- package/dist/types/transport/http/sse-writer.d.ts.map +0 -1
- package/dist/types/transport/in-memory.d.ts.map +0 -1
- package/dist/types/transport/index.d.ts +0 -8
- package/dist/types/transport/index.d.ts.map +0 -1
- package/dist/types/transport/nostr.d.ts.map +0 -1
- package/dist/types/transport/transport.d.ts.map +0 -1
- package/src/runner/index.ts +0 -5
- package/src/transport/http/index.ts +0 -11
- /package/dist/esm/{beacon-strategy.js → core/beacon-strategy.js} +0 -0
- /package/dist/esm/{cohort.js → core/cohort.js} +0 -0
- /package/dist/esm/{conditions.js → core/conditions.js} +0 -0
- /package/dist/esm/{errors.js → core/errors.js} +0 -0
- /package/dist/esm/{fallback-spend.js → core/fallback-spend.js} +0 -0
- /package/dist/esm/{logger.js → core/logger.js} +0 -0
- /package/dist/esm/{messages → core/messages}/base.js +0 -0
- /package/dist/esm/{messages → core/messages}/bodies.js +0 -0
- /package/dist/esm/{messages → core/messages}/constants.js +0 -0
- /package/dist/esm/{messages → core/messages}/factories.js +0 -0
- /package/dist/esm/{messages → core/messages}/guards.js +0 -0
- /package/dist/esm/{messages → core/messages}/index.js +0 -0
- /package/dist/esm/{phases.js → core/phases.js} +0 -0
- /package/dist/esm/{recovery-policy.js → core/recovery-policy.js} +0 -0
- /package/dist/esm/{recovery-spend.js → core/recovery-spend.js} +0 -0
- /package/dist/esm/{signer.js → core/signer.js} +0 -0
- /package/dist/esm/{signing-session.js → core/signing-session.js} +0 -0
- /package/dist/esm/{transport → core/transport}/didcomm.js +0 -0
- /package/dist/esm/{transport → core/transport}/error.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/envelope.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/errors.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/protocol.js +0 -0
- /package/dist/esm/{transport → core/transport}/http/request-auth.js +0 -0
- /package/dist/esm/{transport → core/transport}/in-memory.js +0 -0
- /package/dist/esm/{transport → core/transport}/nostr.js +0 -0
- /package/dist/esm/{transport → core/transport}/transport.js +0 -0
- /package/dist/esm/{runner → core}/typed-emitter.js +0 -0
- /package/dist/esm/{runner → participant}/events.js +0 -0
- /package/dist/esm/{transport/http → participant}/sse-stream.js +0 -0
- /package/dist/esm/{transport/http → service}/inbox-buffer.js +0 -0
- /package/dist/esm/{transport/http → service}/nonce-cache.js +0 -0
- /package/dist/esm/{transport/http → service}/rate-limiter.js +0 -0
- /package/dist/esm/{transport/http → service}/sse-writer.js +0 -0
- /package/dist/types/{conditions.d.ts → core/conditions.d.ts} +0 -0
- /package/dist/types/{errors.d.ts → core/errors.d.ts} +0 -0
- /package/dist/types/{fallback-spend.d.ts → core/fallback-spend.d.ts} +0 -0
- /package/dist/types/{logger.d.ts → core/logger.d.ts} +0 -0
- /package/dist/types/{messages → core/messages}/base.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/bodies.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/constants.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/factories.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/guards.d.ts +0 -0
- /package/dist/types/{messages → core/messages}/index.d.ts +0 -0
- /package/dist/types/{phases.d.ts → core/phases.d.ts} +0 -0
- /package/dist/types/{recovery-policy.d.ts → core/recovery-policy.d.ts} +0 -0
- /package/dist/types/{recovery-spend.d.ts → core/recovery-spend.d.ts} +0 -0
- /package/dist/types/{signer.d.ts → core/signer.d.ts} +0 -0
- /package/dist/types/{signing-session.d.ts → core/signing-session.d.ts} +0 -0
- /package/dist/types/{transport → core/transport}/didcomm.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/error.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/envelope.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/errors.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/protocol.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/http/request-auth.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/in-memory.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/nostr.d.ts +0 -0
- /package/dist/types/{transport → core/transport}/transport.d.ts +0 -0
- /package/dist/types/{runner → core}/typed-emitter.d.ts +0 -0
- /package/dist/types/{transport/http → participant}/sse-stream.d.ts +0 -0
- /package/dist/types/{transport/http → service}/inbox-buffer.d.ts +0 -0
- /package/dist/types/{transport/http → service}/nonce-cache.d.ts +0 -0
- /package/dist/types/{transport/http → service}/rate-limiter.d.ts +0 -0
- /package/dist/types/{transport/http → service}/sse-writer.d.ts +0 -0
- /package/src/{conditions.ts → core/conditions.ts} +0 -0
- /package/src/{errors.ts → core/errors.ts} +0 -0
- /package/src/{fallback-spend.ts → core/fallback-spend.ts} +0 -0
- /package/src/{logger.ts → core/logger.ts} +0 -0
- /package/src/{messages → core/messages}/base.ts +0 -0
- /package/src/{messages → core/messages}/bodies.ts +0 -0
- /package/src/{messages → core/messages}/constants.ts +0 -0
- /package/src/{messages → core/messages}/factories.ts +0 -0
- /package/src/{messages → core/messages}/guards.ts +0 -0
- /package/src/{messages → core/messages}/index.ts +0 -0
- /package/src/{phases.ts → core/phases.ts} +0 -0
- /package/src/{recovery-policy.ts → core/recovery-policy.ts} +0 -0
- /package/src/{recovery-spend.ts → core/recovery-spend.ts} +0 -0
- /package/src/{signer.ts → core/signer.ts} +0 -0
- /package/src/{signing-session.ts → core/signing-session.ts} +0 -0
- /package/src/{transport → core/transport}/didcomm.ts +0 -0
- /package/src/{transport → core/transport}/error.ts +0 -0
- /package/src/{transport → core/transport}/http/envelope.ts +0 -0
- /package/src/{transport → core/transport}/http/errors.ts +0 -0
- /package/src/{transport → core/transport}/http/protocol.ts +0 -0
- /package/src/{transport → core/transport}/http/request-auth.ts +0 -0
- /package/src/{transport → core/transport}/in-memory.ts +0 -0
- /package/src/{transport → core/transport}/nostr.ts +0 -0
- /package/src/{transport → core/transport}/transport.ts +0 -0
- /package/src/{runner → core}/typed-emitter.ts +0 -0
- /package/src/{transport/http → participant}/sse-stream.ts +0 -0
- /package/src/{transport/http → service}/inbox-buffer.ts +0 -0
- /package/src/{transport/http → service}/nonce-cache.ts +0 -0
- /package/src/{transport/http → service}/rate-limiter.ts +0 -0
- /package/src/{transport/http → service}/sse-writer.ts +0 -0
|
@@ -0,0 +1,3199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/service/index.ts
|
|
31
|
+
var service_exports = {};
|
|
32
|
+
__export(service_exports, {
|
|
33
|
+
AggregationService: () => AggregationService,
|
|
34
|
+
AggregationServiceRunner: () => AggregationServiceRunner,
|
|
35
|
+
DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
36
|
+
DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
37
|
+
HttpServerTransport: () => HttpServerTransport,
|
|
38
|
+
InMemoryRateLimitStore: () => InMemoryRateLimitStore,
|
|
39
|
+
InboxBuffer: () => InboxBuffer,
|
|
40
|
+
NonceCache: () => NonceCache,
|
|
41
|
+
RateLimiter: () => RateLimiter,
|
|
42
|
+
formatSseComment: () => formatSseComment,
|
|
43
|
+
formatSseEvent: () => formatSseEvent
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(service_exports);
|
|
46
|
+
|
|
47
|
+
// src/service/service.ts
|
|
48
|
+
var import_common4 = require("@did-btcr2/common");
|
|
49
|
+
var import_cryptosuite = require("@did-btcr2/cryptosuite");
|
|
50
|
+
var import_secp256k14 = require("@noble/curves/secp256k1.js");
|
|
51
|
+
var import_utils3 = require("@noble/hashes/utils");
|
|
52
|
+
|
|
53
|
+
// src/core/beacon-strategy.ts
|
|
54
|
+
var import_common = require("@did-btcr2/common");
|
|
55
|
+
var import_smt = require("@did-btcr2/smt");
|
|
56
|
+
var CAS_STRATEGY = {
|
|
57
|
+
type: "CASBeacon",
|
|
58
|
+
buildAggregatedData(cohort) {
|
|
59
|
+
cohort.buildCASAnnouncement();
|
|
60
|
+
},
|
|
61
|
+
getDistributePayload(cohort) {
|
|
62
|
+
return { casAnnouncement: cohort.casAnnouncement };
|
|
63
|
+
},
|
|
64
|
+
validateParticipantView({ participantDid, included, expectedHash, body }) {
|
|
65
|
+
const casAnnouncement = body.casAnnouncement;
|
|
66
|
+
if (!casAnnouncement) return { matches: false };
|
|
67
|
+
if (!included) {
|
|
68
|
+
return { matches: !(participantDid in casAnnouncement), casAnnouncement };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
matches: casAnnouncement[participantDid] === expectedHash,
|
|
72
|
+
casAnnouncement
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var SMT_STRATEGY = {
|
|
77
|
+
type: "SMTBeacon",
|
|
78
|
+
buildAggregatedData(cohort) {
|
|
79
|
+
cohort.buildSMTTree();
|
|
80
|
+
},
|
|
81
|
+
getDistributePayload(cohort, participantDid) {
|
|
82
|
+
const proof = cohort.smtProofs?.get(participantDid);
|
|
83
|
+
return { smtProof: proof };
|
|
84
|
+
},
|
|
85
|
+
validateParticipantView({ participantDid, included, submittedUpdate, body }) {
|
|
86
|
+
const smtProof = body.smtProof;
|
|
87
|
+
const index = (0, import_smt.didToIndex)(participantDid);
|
|
88
|
+
if (!included) {
|
|
89
|
+
if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
|
|
90
|
+
const candidateHash2 = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)));
|
|
91
|
+
return { matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash2), smtProof };
|
|
92
|
+
}
|
|
93
|
+
if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
|
|
94
|
+
const canonicalBytes = new TextEncoder().encode((0, import_common.canonicalize)(submittedUpdate));
|
|
95
|
+
const expectedUpdateId = (0, import_smt.hashToBase64Url)((0, import_smt.blockHash)(canonicalBytes));
|
|
96
|
+
if (smtProof.updateId !== expectedUpdateId) {
|
|
97
|
+
return { matches: false, smtProof };
|
|
98
|
+
}
|
|
99
|
+
const candidateHash = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)), (0, import_smt.base64UrlToHash)(smtProof.updateId));
|
|
100
|
+
return {
|
|
101
|
+
matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash),
|
|
102
|
+
smtProof
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var STRATEGIES = /* @__PURE__ */ new Map([
|
|
107
|
+
[CAS_STRATEGY.type, CAS_STRATEGY],
|
|
108
|
+
[SMT_STRATEGY.type, SMT_STRATEGY]
|
|
109
|
+
]);
|
|
110
|
+
function getBeaconStrategy(type) {
|
|
111
|
+
return STRATEGIES.get(type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/core/cohort.ts
|
|
115
|
+
var import_bitcoin = require("@did-btcr2/bitcoin");
|
|
116
|
+
var import_common3 = require("@did-btcr2/common");
|
|
117
|
+
var import_smt2 = require("@did-btcr2/smt");
|
|
118
|
+
var import_secp256k12 = require("@noble/curves/secp256k1.js");
|
|
119
|
+
var import_utils2 = require("@noble/hashes/utils");
|
|
120
|
+
var import_btc_signer2 = require("@scure/btc-signer");
|
|
121
|
+
var import_musig22 = require("@scure/btc-signer/musig2");
|
|
122
|
+
|
|
123
|
+
// src/core/errors.ts
|
|
124
|
+
var import_common2 = require("@did-btcr2/common");
|
|
125
|
+
var AggregationServiceError = class extends import_common2.MethodError {
|
|
126
|
+
constructor(message, type = "AggregationServiceError", data) {
|
|
127
|
+
super(message, type, data);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var AggregationCohortError = class extends import_common2.MethodError {
|
|
131
|
+
constructor(message, type = "AggregationCohortError", data) {
|
|
132
|
+
super(message, type, data);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var SigningSessionError = class extends import_common2.MethodError {
|
|
136
|
+
constructor(message, type = "SigningSessionError", data) {
|
|
137
|
+
super(message, type, data);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/core/recovery-policy.ts
|
|
142
|
+
var import_secp256k1 = require("@noble/curves/secp256k1.js");
|
|
143
|
+
var import_utils = require("@noble/hashes/utils");
|
|
144
|
+
var import_btc_signer = require("@scure/btc-signer");
|
|
145
|
+
var import_musig2 = require("@scure/btc-signer/musig2");
|
|
146
|
+
var DEFAULT_FUNDING_MODEL = "operator-funded";
|
|
147
|
+
var MAX_RECOVERY_SEQUENCE = 65535;
|
|
148
|
+
var TAPROOT_LEAF_VERSION = 192;
|
|
149
|
+
function compactSize(n) {
|
|
150
|
+
if (n < 253) return new Uint8Array([n]);
|
|
151
|
+
if (n <= 65535) return new Uint8Array([253, n & 255, n >> 8 & 255]);
|
|
152
|
+
return new Uint8Array([254, n & 255, n >> 8 & 255, n >> 16 & 255, n >> 24 & 255]);
|
|
153
|
+
}
|
|
154
|
+
function tapLeafHash(script, leafVersion = TAPROOT_LEAF_VERSION) {
|
|
155
|
+
return import_secp256k1.schnorr.utils.taggedHash(
|
|
156
|
+
"TapLeaf",
|
|
157
|
+
(0, import_utils.concatBytes)(new Uint8Array([leafVersion]), compactSize(script.length), script)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function resolveFallbackThreshold(advertised, n) {
|
|
161
|
+
return advertised ?? Math.max(1, n - 1);
|
|
162
|
+
}
|
|
163
|
+
function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
|
|
164
|
+
const n = cohortKeys.length;
|
|
165
|
+
if (n === 0) {
|
|
166
|
+
throw new AggregationCohortError(
|
|
167
|
+
"Cannot build fallback leaf: no cohort keys.",
|
|
168
|
+
"NO_COHORT_KEYS"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
for (const key of cohortKeys) {
|
|
172
|
+
if (key.length !== 33) {
|
|
173
|
+
throw new AggregationCohortError(
|
|
174
|
+
`Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
|
|
175
|
+
"INVALID_COHORT_KEY",
|
|
176
|
+
{ length: key.length }
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n) {
|
|
181
|
+
throw new AggregationCohortError(
|
|
182
|
+
`Fallback threshold must be an integer in [1, ${n}] (k-of-n), got ${fallbackThreshold}.`,
|
|
183
|
+
"INVALID_FALLBACK_THRESHOLD",
|
|
184
|
+
{ fallbackThreshold, n }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function buildFallbackLeaf(params) {
|
|
189
|
+
assertFallbackParams(params);
|
|
190
|
+
const xOnlyKeys = (0, import_musig2.sortKeys)(params.cohortKeys).map((k) => k.slice(1));
|
|
191
|
+
return (0, import_btc_signer.p2tr_ms)(params.fallbackThreshold, xOnlyKeys).script;
|
|
192
|
+
}
|
|
193
|
+
function assertRecoveryParams({ recoveryKey, recoverySequence }) {
|
|
194
|
+
if (recoveryKey.length !== 32) {
|
|
195
|
+
throw new AggregationCohortError(
|
|
196
|
+
`Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
|
|
197
|
+
"INVALID_RECOVERY_KEY"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
201
|
+
throw new AggregationCohortError(
|
|
202
|
+
`Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
|
|
203
|
+
"INVALID_RECOVERY_SEQUENCE"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function buildRecoveryScript(params) {
|
|
208
|
+
assertRecoveryParams(params);
|
|
209
|
+
return import_btc_signer.Script.encode([
|
|
210
|
+
params.recoverySequence,
|
|
211
|
+
"CHECKSEQUENCEVERIFY",
|
|
212
|
+
"DROP",
|
|
213
|
+
params.recoveryKey,
|
|
214
|
+
"CHECKSIG"
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
function buildRecoveryLeaves(fundingModel, params) {
|
|
218
|
+
switch (fundingModel) {
|
|
219
|
+
case "operator-funded":
|
|
220
|
+
return [
|
|
221
|
+
{ script: buildFallbackLeaf(params) },
|
|
222
|
+
{ script: buildRecoveryScript(params) }
|
|
223
|
+
];
|
|
224
|
+
case "participant-funded":
|
|
225
|
+
throw new AggregationCohortError(
|
|
226
|
+
"Funding model 'participant-funded' is reserved and not yet implemented.",
|
|
227
|
+
"UNSUPPORTED_FUNDING_MODEL",
|
|
228
|
+
{ fundingModel }
|
|
229
|
+
);
|
|
230
|
+
default:
|
|
231
|
+
throw new AggregationCohortError(
|
|
232
|
+
`Unknown funding model: ${fundingModel}.`,
|
|
233
|
+
"UNKNOWN_FUNDING_MODEL",
|
|
234
|
+
{ fundingModel }
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/core/cohort.ts
|
|
240
|
+
var AggregationCohort = class {
|
|
241
|
+
/** Unique identifier for the cohort. */
|
|
242
|
+
id;
|
|
243
|
+
/** DID of the Aggregation Service managing this cohort. */
|
|
244
|
+
serviceDid;
|
|
245
|
+
/** Minimum number of participants required to finalize the cohort. */
|
|
246
|
+
minParticipants;
|
|
247
|
+
/** Network on which the cohort operates (mainnet, mutinynet, etc.). */
|
|
248
|
+
network;
|
|
249
|
+
/** Type of beacon used in the cohort: 'CASBeacon' or 'SMTBeacon'. */
|
|
250
|
+
beaconType;
|
|
251
|
+
/** List of participant DIDs that have been accepted into the cohort. */
|
|
252
|
+
participants = [];
|
|
253
|
+
/**
|
|
254
|
+
* Mapping from participant DID to their compressed secp256k1 public key.
|
|
255
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
|
|
256
|
+
* callers look up a participant's key without knowing their position in the
|
|
257
|
+
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
258
|
+
*/
|
|
259
|
+
participantKeys = /* @__PURE__ */ new Map();
|
|
260
|
+
/** Sorted list of cohort participants' compressed public keys. */
|
|
261
|
+
#cohortKeys = [];
|
|
262
|
+
/**
|
|
263
|
+
* BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
|
|
264
|
+
* The beacon output is an internal key (the MuSig2 aggregate) plus a script
|
|
265
|
+
* tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
|
|
266
|
+
* The MuSig2 signing session applies this as an x-only tweak; a value that does
|
|
267
|
+
* not match the root the funded address was derived from silently yields an
|
|
268
|
+
* invalid key-path signature.
|
|
269
|
+
*/
|
|
270
|
+
tapTweak = new Uint8Array();
|
|
271
|
+
/** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
|
|
272
|
+
internalKey = new Uint8Array();
|
|
273
|
+
/** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
|
|
274
|
+
tapMerkleRoot = new Uint8Array();
|
|
275
|
+
/** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
|
|
276
|
+
recoveryKey;
|
|
277
|
+
/** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
|
|
278
|
+
recoverySequence;
|
|
279
|
+
/** Funding model governing the recovery leaves (default 'operator-funded'). */
|
|
280
|
+
fundingModel;
|
|
281
|
+
/**
|
|
282
|
+
* Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
|
|
283
|
+
* address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
|
|
284
|
+
*/
|
|
285
|
+
fallbackThreshold;
|
|
286
|
+
/** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
|
|
287
|
+
beaconAddress = "";
|
|
288
|
+
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
289
|
+
pendingUpdates = /* @__PURE__ */ new Map();
|
|
290
|
+
/**
|
|
291
|
+
* Participant DIDs that explicitly declined to submit an update this round
|
|
292
|
+
* (cooperative non-inclusion). A decliner is absent from the CAS Announcement
|
|
293
|
+
* Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
|
|
294
|
+
* disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
|
|
295
|
+
*/
|
|
296
|
+
nonIncluded = /* @__PURE__ */ new Set();
|
|
297
|
+
/** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
|
|
298
|
+
casAnnouncement;
|
|
299
|
+
/** Per-participant SMT proofs, set by buildSMTTree(). */
|
|
300
|
+
smtProofs;
|
|
301
|
+
/** Signal bytes (32 bytes) for OP_RETURN: SHA-256 of CAS announcement OR SMT root. */
|
|
302
|
+
signalBytes;
|
|
303
|
+
/** Set of participant DIDs that have approved the aggregated data. */
|
|
304
|
+
validationAcks = /* @__PURE__ */ new Set();
|
|
305
|
+
/** Set of participant DIDs that have rejected the aggregated data. */
|
|
306
|
+
validationRejections = /* @__PURE__ */ new Set();
|
|
307
|
+
constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
|
|
308
|
+
this.id = id || crypto.randomUUID();
|
|
309
|
+
this.minParticipants = minParticipants ?? 2;
|
|
310
|
+
this.serviceDid = serviceDid || "";
|
|
311
|
+
this.network = network;
|
|
312
|
+
this.beaconType = beaconType || "CASBeacon";
|
|
313
|
+
this.recoveryKey = recoveryKey;
|
|
314
|
+
this.recoverySequence = recoverySequence;
|
|
315
|
+
this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
|
|
316
|
+
this.fallbackThreshold = fallbackThreshold;
|
|
317
|
+
}
|
|
318
|
+
/** Sorted cohort keys (sorted on assignment per BIP-327). */
|
|
319
|
+
get cohortKeys() {
|
|
320
|
+
return this.#cohortKeys;
|
|
321
|
+
}
|
|
322
|
+
set cohortKeys(keys) {
|
|
323
|
+
this.#cohortKeys = (0, import_musig22.sortKeys)(keys);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The resolved k of the k-of-n fallback leaf: the advertised
|
|
327
|
+
* {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
|
|
328
|
+
* the current cohort size. This is the value the beacon address commits to and
|
|
329
|
+
* the spend builders must reproduce. Returns 0 before any cohort keys are set.
|
|
330
|
+
*/
|
|
331
|
+
get effectiveFallbackThreshold() {
|
|
332
|
+
if (this.#cohortKeys.length === 0) return 0;
|
|
333
|
+
return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Computes the Taproot beacon address from the cohort keys and recovery params.
|
|
337
|
+
*
|
|
338
|
+
* The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
|
|
339
|
+
* script path is the recovery tree (a CSV recovery leaf so a missing signer
|
|
340
|
+
* cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
|
|
341
|
+
* the `tapTweak` the MuSig2 session must apply for the key-path spend.
|
|
342
|
+
*
|
|
343
|
+
* The tweak is derived from the Merkle root the address was built with (read
|
|
344
|
+
* back from the payment), never recomputed by hand, so the MuSig2 key-path
|
|
345
|
+
* signature is guaranteed to validate against the funded address.
|
|
346
|
+
*/
|
|
347
|
+
computeBeaconAddress() {
|
|
348
|
+
if (this.#cohortKeys.length === 0) {
|
|
349
|
+
throw new AggregationCohortError(
|
|
350
|
+
"Cannot compute beacon address: no cohort keys.",
|
|
351
|
+
"NO_COHORT_KEYS",
|
|
352
|
+
{ cohortId: this.id }
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
|
|
356
|
+
throw new AggregationCohortError(
|
|
357
|
+
"Cannot compute beacon address: missing recovery key or sequence.",
|
|
358
|
+
"NO_RECOVERY_PARAMS",
|
|
359
|
+
{ cohortId: this.id }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const keyAggContext = (0, import_musig22.keyAggregate)(this.#cohortKeys);
|
|
363
|
+
const aggPubkey = (0, import_musig22.keyAggExport)(keyAggContext);
|
|
364
|
+
const leaves = buildRecoveryLeaves(this.fundingModel, {
|
|
365
|
+
recoveryKey: this.recoveryKey,
|
|
366
|
+
recoverySequence: this.recoverySequence,
|
|
367
|
+
cohortKeys: this.#cohortKeys,
|
|
368
|
+
fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
|
|
369
|
+
});
|
|
370
|
+
const payment = (0, import_btc_signer2.p2tr)(aggPubkey, leaves, (0, import_bitcoin.getNetwork)(this.network), true);
|
|
371
|
+
this.internalKey = aggPubkey;
|
|
372
|
+
this.tapMerkleRoot = payment.tapMerkleRoot;
|
|
373
|
+
this.tapTweak = import_secp256k12.schnorr.utils.taggedHash("TapTweak", (0, import_utils2.concatBytes)(aggPubkey, payment.tapMerkleRoot));
|
|
374
|
+
if (!payment.address) {
|
|
375
|
+
throw new AggregationCohortError(
|
|
376
|
+
"Failed to compute Taproot address",
|
|
377
|
+
"BEACON_ADDRESS_ERROR",
|
|
378
|
+
{ cohortId: this.id }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
this.beaconAddress = payment.address;
|
|
382
|
+
return payment.address;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Validates that the participant's key is in the cohort and the beacon address
|
|
386
|
+
* matches the locally-computed one. Used by participants to verify cohort ready
|
|
387
|
+
* messages from the service.
|
|
388
|
+
*/
|
|
389
|
+
validateMembership(participantPkHex, cohortKeysHex, expectedBeaconAddress) {
|
|
390
|
+
if (!cohortKeysHex.includes(participantPkHex)) {
|
|
391
|
+
throw new AggregationCohortError(
|
|
392
|
+
`Participant key not found in cohort ${this.id}.`,
|
|
393
|
+
"COHORT_VALIDATION_ERROR",
|
|
394
|
+
{ cohortId: this.id, participantPkHex }
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
this.cohortKeys = cohortKeysHex.map((k) => (0, import_utils2.hexToBytes)(k));
|
|
398
|
+
const computed = this.computeBeaconAddress();
|
|
399
|
+
if (computed !== expectedBeaconAddress) {
|
|
400
|
+
throw new AggregationCohortError(
|
|
401
|
+
`Computed beacon address ${computed} does not match expected ${expectedBeaconAddress}.`,
|
|
402
|
+
"BEACON_ADDRESS_MISMATCH",
|
|
403
|
+
{ cohortId: this.id, computed, expected: expectedBeaconAddress }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Returns the position of a participant's public key in the sorted
|
|
409
|
+
* {@link cohortKeys} array, or -1 if the participant is not in the cohort.
|
|
410
|
+
* Required by MuSig2 partial-sig verification which indexes by signer position.
|
|
411
|
+
*/
|
|
412
|
+
indexOfParticipant(did) {
|
|
413
|
+
const pk = this.participantKeys.get(did);
|
|
414
|
+
if (!pk) return -1;
|
|
415
|
+
return this.#cohortKeys.findIndex(
|
|
416
|
+
(k) => k.length === pk.length && k.every((b, i) => b === pk[i])
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
addUpdate(participantDid, signedUpdate) {
|
|
420
|
+
if (!this.participants.includes(participantDid)) {
|
|
421
|
+
throw new AggregationCohortError(
|
|
422
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
423
|
+
"UNKNOWN_PARTICIPANT",
|
|
424
|
+
{ cohortId: this.id, participantDid }
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
this.pendingUpdates.set(participantDid, signedUpdate);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Record that a participant declined to submit an update this round
|
|
431
|
+
* (cooperative non-inclusion). The member stays in the cohort and still signs.
|
|
432
|
+
*/
|
|
433
|
+
addNonInclusion(participantDid) {
|
|
434
|
+
if (!this.participants.includes(participantDid)) {
|
|
435
|
+
throw new AggregationCohortError(
|
|
436
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
437
|
+
"UNKNOWN_PARTICIPANT",
|
|
438
|
+
{ cohortId: this.id, participantDid }
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (this.pendingUpdates.has(participantDid)) {
|
|
442
|
+
throw new AggregationCohortError(
|
|
443
|
+
`Participant ${participantDid} already submitted an update; cannot also decline.`,
|
|
444
|
+
"CONFLICTING_RESPONSE",
|
|
445
|
+
{ cohortId: this.id, participantDid }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
this.nonIncluded.add(participantDid);
|
|
449
|
+
}
|
|
450
|
+
hasAllUpdates() {
|
|
451
|
+
return this.pendingUpdates.size === this.participants.length;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* True when every participant has responded for this round, either with an
|
|
455
|
+
* update or with an explicit non-inclusion. This is the aggregation gate when
|
|
456
|
+
* non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
|
|
457
|
+
* {@link hasAllValidationResponses} generalizes a unanimous ack.
|
|
458
|
+
*/
|
|
459
|
+
hasAllResponses() {
|
|
460
|
+
return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Builds a CAS Announcement Map from collected updates.
|
|
464
|
+
* Maps each participant DID to base64url canonical hash of their signed update.
|
|
465
|
+
* Computes signal bytes as SHA-256 of canonicalized announcement.
|
|
466
|
+
*
|
|
467
|
+
* Members who declined (cooperative non-inclusion) are naturally absent from
|
|
468
|
+
* the map: the body iterates {@link pendingUpdates}, which never holds a
|
|
469
|
+
* decliner. Absence from the map is exactly the CAS non-inclusion signal.
|
|
470
|
+
*/
|
|
471
|
+
buildCASAnnouncement() {
|
|
472
|
+
if (!this.hasAllResponses()) {
|
|
473
|
+
throw new AggregationCohortError(
|
|
474
|
+
"Cannot build CAS Announcement: not all participants have responded.",
|
|
475
|
+
"INCOMPLETE_RESPONSES",
|
|
476
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const announcement = {};
|
|
480
|
+
for (const [did, signedUpdate] of this.pendingUpdates) {
|
|
481
|
+
announcement[did] = (0, import_common3.canonicalHash)(signedUpdate);
|
|
482
|
+
}
|
|
483
|
+
this.casAnnouncement = announcement;
|
|
484
|
+
this.signalBytes = (0, import_common3.hash)((0, import_common3.canonicalize)(announcement));
|
|
485
|
+
return announcement;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Builds an SMT tree with one leaf per participant.
|
|
489
|
+
*
|
|
490
|
+
* A member who submitted an update gets an inclusion leaf
|
|
491
|
+
* (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
|
|
492
|
+
* non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
|
|
493
|
+
* omitted). The cohort mints each member's nonce and returns it inside that
|
|
494
|
+
* member's serialized proof, so a decliner can self-validate its own
|
|
495
|
+
* non-inclusion slot and the resolver can recompute the leaf. Stores
|
|
496
|
+
* per-participant proofs and the SMT root as signalBytes.
|
|
497
|
+
*/
|
|
498
|
+
buildSMTTree() {
|
|
499
|
+
if (!this.hasAllResponses()) {
|
|
500
|
+
throw new AggregationCohortError(
|
|
501
|
+
"Cannot build SMT tree: not all participants have responded.",
|
|
502
|
+
"INCOMPLETE_RESPONSES",
|
|
503
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const tree = new import_smt2.BTCR2MerkleTree();
|
|
507
|
+
const entries = [];
|
|
508
|
+
const encoder = new TextEncoder();
|
|
509
|
+
for (const did of this.participants) {
|
|
510
|
+
const nonce = (0, import_utils2.randomBytes)(32);
|
|
511
|
+
const signedUpdate = this.pendingUpdates.get(did);
|
|
512
|
+
if (signedUpdate) {
|
|
513
|
+
entries.push({ did, nonce, signedUpdate: encoder.encode((0, import_common3.canonicalize)(signedUpdate)) });
|
|
514
|
+
} else {
|
|
515
|
+
entries.push({ did, nonce });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
tree.addEntries(entries);
|
|
519
|
+
tree.finalize();
|
|
520
|
+
this.signalBytes = tree.rootHash;
|
|
521
|
+
this.smtProofs = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const did of this.participants) {
|
|
523
|
+
this.smtProofs.set(did, tree.proof(did));
|
|
524
|
+
}
|
|
525
|
+
return this.smtProofs;
|
|
526
|
+
}
|
|
527
|
+
addValidation(participantDid, approved) {
|
|
528
|
+
if (!this.participants.includes(participantDid)) {
|
|
529
|
+
throw new AggregationCohortError(
|
|
530
|
+
`Unknown participant ${participantDid} in cohort ${this.id}.`,
|
|
531
|
+
"UNKNOWN_PARTICIPANT",
|
|
532
|
+
{ cohortId: this.id, participantDid }
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (approved) {
|
|
536
|
+
this.validationAcks.add(participantDid);
|
|
537
|
+
} else {
|
|
538
|
+
this.validationRejections.add(participantDid);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* True when every participant has either approved or rejected the aggregated data.
|
|
543
|
+
*/
|
|
544
|
+
hasAllValidationResponses() {
|
|
545
|
+
return this.validationAcks.size + this.validationRejections.size === this.participants.length;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* True when all participants approved. Note: differs from {@link hasAllValidationResponses},
|
|
549
|
+
* this returns false if any participant rejected, even if all responses are in.
|
|
550
|
+
*/
|
|
551
|
+
isFullyValidated() {
|
|
552
|
+
return this.validationRejections.size === 0 && this.validationAcks.size === this.participants.length;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/core/conditions.ts
|
|
557
|
+
var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
|
|
558
|
+
var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
|
|
559
|
+
function checkPair(problems, label, min, max) {
|
|
560
|
+
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
561
|
+
problems.push(`min${label} must be an integer >= 0`);
|
|
562
|
+
}
|
|
563
|
+
if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
|
|
564
|
+
problems.push(`max${label} must be an integer >= 0`);
|
|
565
|
+
}
|
|
566
|
+
if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
|
|
567
|
+
problems.push(`max${label} must be >= min${label}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function checkCost(problems, label, cost) {
|
|
571
|
+
if (cost === void 0) return;
|
|
572
|
+
if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
|
|
573
|
+
problems.push(`${label}.amount must be a finite number >= 0`);
|
|
574
|
+
}
|
|
575
|
+
if (typeof cost.unit !== "string" || cost.unit.length === 0) {
|
|
576
|
+
problems.push(`${label}.unit must be a non-empty string`);
|
|
577
|
+
}
|
|
578
|
+
if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
|
|
579
|
+
problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function validateCohortConditions(c) {
|
|
583
|
+
const problems = [];
|
|
584
|
+
if (!KNOWN_BEACON_TYPES.includes(c.beaconType)) {
|
|
585
|
+
problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
|
|
586
|
+
}
|
|
587
|
+
if (!Number.isInteger(c.minParticipants) || c.minParticipants < 1) {
|
|
588
|
+
problems.push("minParticipants must be an integer >= 1");
|
|
589
|
+
}
|
|
590
|
+
if (c.maxParticipants !== void 0) {
|
|
591
|
+
if (!Number.isInteger(c.maxParticipants) || c.maxParticipants < 1) {
|
|
592
|
+
problems.push("maxParticipants must be an integer >= 1");
|
|
593
|
+
} else if (Number.isInteger(c.minParticipants) && c.maxParticipants < c.minParticipants) {
|
|
594
|
+
problems.push("maxParticipants must be >= minParticipants");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
checkPair(problems, "DidsPerParticipant", c.minDidsPerParticipant, c.maxDidsPerParticipant);
|
|
598
|
+
checkPair(problems, "SecondsBetweenAnnouncements", c.minSecondsBetweenAnnouncements, c.maxSecondsBetweenAnnouncements);
|
|
599
|
+
if (c.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c.pendingUpdateTrigger) || c.pendingUpdateTrigger < 1)) {
|
|
600
|
+
problems.push("pendingUpdateTrigger must be an integer >= 1");
|
|
601
|
+
}
|
|
602
|
+
if (c.fallbackThreshold !== void 0) {
|
|
603
|
+
if (!Number.isInteger(c.fallbackThreshold) || c.fallbackThreshold < 1) {
|
|
604
|
+
problems.push("fallbackThreshold must be an integer >= 1");
|
|
605
|
+
} else if (c.maxParticipants !== void 0 && Number.isInteger(c.maxParticipants) && c.fallbackThreshold > c.maxParticipants) {
|
|
606
|
+
problems.push("fallbackThreshold must be <= maxParticipants");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
checkCost(problems, "costOfEnrollment", c.costOfEnrollment);
|
|
610
|
+
checkCost(problems, "costPerAnnouncement", c.costPerAnnouncement);
|
|
611
|
+
if (typeof c.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c.recoveryKey)) {
|
|
612
|
+
problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
|
|
613
|
+
}
|
|
614
|
+
if (!Number.isInteger(c.recoverySequence) || c.recoverySequence < 1 || c.recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
615
|
+
problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
|
|
616
|
+
}
|
|
617
|
+
if (c.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c.fundingModel)) {
|
|
618
|
+
problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
|
|
619
|
+
}
|
|
620
|
+
return problems;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/core/fallback-spend.ts
|
|
624
|
+
var import_bitcoin2 = require("@did-btcr2/bitcoin");
|
|
625
|
+
var import_secp256k13 = require("@noble/curves/secp256k1.js");
|
|
626
|
+
var import_btc_signer3 = require("@scure/btc-signer");
|
|
627
|
+
var import_musig23 = require("@scure/btc-signer/musig2");
|
|
628
|
+
function bytesEqual(a, b) {
|
|
629
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
630
|
+
}
|
|
631
|
+
function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
|
|
632
|
+
return tx.preimageWitnessV1(
|
|
633
|
+
inputIndex,
|
|
634
|
+
[prevOutScript],
|
|
635
|
+
import_btc_signer3.SigHash.DEFAULT,
|
|
636
|
+
[prevOutValue],
|
|
637
|
+
void 0,
|
|
638
|
+
fallbackLeafScript,
|
|
639
|
+
TAPROOT_LEAF_VERSION
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
function buildFallbackSpend(params) {
|
|
643
|
+
const {
|
|
644
|
+
pendingTx,
|
|
645
|
+
cohortKeys,
|
|
646
|
+
recoveryKey,
|
|
647
|
+
recoverySequence,
|
|
648
|
+
fundingModel,
|
|
649
|
+
network,
|
|
650
|
+
prevOutScript,
|
|
651
|
+
prevOutValue,
|
|
652
|
+
signatures
|
|
653
|
+
} = params;
|
|
654
|
+
const inputIndex = params.inputIndex ?? 0;
|
|
655
|
+
if (cohortKeys.length === 0) {
|
|
656
|
+
throw new AggregationCohortError(
|
|
657
|
+
"Cannot build fallback spend: no cohort keys.",
|
|
658
|
+
"NO_COHORT_KEYS"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
|
|
662
|
+
const internalKey = (0, import_musig23.keyAggExport)((0, import_musig23.keyAggregate)((0, import_musig23.sortKeys)(cohortKeys)));
|
|
663
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
664
|
+
recoveryKey,
|
|
665
|
+
recoverySequence,
|
|
666
|
+
cohortKeys,
|
|
667
|
+
fallbackThreshold
|
|
668
|
+
});
|
|
669
|
+
const net = (0, import_bitcoin2.getNetwork)(network);
|
|
670
|
+
const payment = (0, import_btc_signer3.p2tr)(internalKey, leaves, net, true);
|
|
671
|
+
if (!bytesEqual(payment.script, prevOutScript)) {
|
|
672
|
+
throw new AggregationCohortError(
|
|
673
|
+
"Reconstructed beacon output script does not match the spent prevout script.",
|
|
674
|
+
"PREVOUT_SCRIPT_MISMATCH"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
|
|
678
|
+
const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
|
|
679
|
+
const script = scriptVer.slice(0, scriptVer.length - 1);
|
|
680
|
+
return bytesEqual(script, fallbackLeaf);
|
|
681
|
+
});
|
|
682
|
+
if (!leafEntry) {
|
|
683
|
+
throw new AggregationCohortError(
|
|
684
|
+
"Could not locate the fallback leaf in the reconstructed beacon output.",
|
|
685
|
+
"FALLBACK_LEAF_NOT_FOUND"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const cohortXOnly = (0, import_musig23.sortKeys)(cohortKeys).map((k) => k.slice(1));
|
|
689
|
+
const tx = pendingTx;
|
|
690
|
+
const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
|
|
691
|
+
const leafHash = tapLeafHash(fallbackLeaf);
|
|
692
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const { pubKey, signature } of signatures) {
|
|
694
|
+
if (pubKey.length !== 32 || signature.length !== 64) continue;
|
|
695
|
+
const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
|
|
696
|
+
if (!isCohortKey) continue;
|
|
697
|
+
const hex = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
698
|
+
if (accepted.has(hex)) continue;
|
|
699
|
+
let ok = false;
|
|
700
|
+
try {
|
|
701
|
+
ok = import_secp256k13.schnorr.verify(signature, sighash, pubKey);
|
|
702
|
+
} catch {
|
|
703
|
+
ok = false;
|
|
704
|
+
}
|
|
705
|
+
if (ok) accepted.set(hex, signature);
|
|
706
|
+
}
|
|
707
|
+
if (accepted.size < fallbackThreshold) {
|
|
708
|
+
throw new AggregationCohortError(
|
|
709
|
+
`Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
|
|
710
|
+
"NOT_ENOUGH_FALLBACK_SIGNATURES",
|
|
711
|
+
{ have: accepted.size, need: fallbackThreshold }
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
|
|
715
|
+
tx.updateInput(inputIndex, {
|
|
716
|
+
tapLeafScript: [leafEntry],
|
|
717
|
+
tapScriptSig: chosen.map((pubKey) => {
|
|
718
|
+
const hex = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
719
|
+
return [{ pubKey, leafHash }, accepted.get(hex)];
|
|
720
|
+
})
|
|
721
|
+
});
|
|
722
|
+
tx.finalize();
|
|
723
|
+
return tx;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/core/messages/base.ts
|
|
727
|
+
var AGGREGATION_WIRE_VERSION = 1;
|
|
728
|
+
var BaseMessage = class {
|
|
729
|
+
type;
|
|
730
|
+
version;
|
|
731
|
+
to;
|
|
732
|
+
from;
|
|
733
|
+
body;
|
|
734
|
+
constructor({ type, version, to, from, body }) {
|
|
735
|
+
this.type = type;
|
|
736
|
+
this.version = version ?? AGGREGATION_WIRE_VERSION;
|
|
737
|
+
this.to = to;
|
|
738
|
+
this.from = from;
|
|
739
|
+
this.body = body;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Converts a BaseMessage to a JSON object.
|
|
743
|
+
* @returns {Base} The JSON representation of the BaseMessage.
|
|
744
|
+
*/
|
|
745
|
+
toJSON() {
|
|
746
|
+
return {
|
|
747
|
+
type: this.type,
|
|
748
|
+
version: this.version,
|
|
749
|
+
to: this.to,
|
|
750
|
+
from: this.from,
|
|
751
|
+
body: this.body
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/core/messages/constants.ts
|
|
757
|
+
var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
|
|
758
|
+
var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
|
|
759
|
+
var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
760
|
+
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
761
|
+
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
762
|
+
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
763
|
+
var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
|
|
764
|
+
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
765
|
+
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
766
|
+
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
767
|
+
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
768
|
+
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
769
|
+
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
770
|
+
var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
|
|
771
|
+
var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
|
|
772
|
+
|
|
773
|
+
// src/core/messages/factories.ts
|
|
774
|
+
function createCohortAdvertMessage(fields) {
|
|
775
|
+
const { from, ...body } = fields;
|
|
776
|
+
return new BaseMessage({ type: COHORT_ADVERT, from, body });
|
|
777
|
+
}
|
|
778
|
+
function createCohortOptInAcceptMessage(fields) {
|
|
779
|
+
const { from, to, ...body } = fields;
|
|
780
|
+
return new BaseMessage({ type: COHORT_OPT_IN_ACCEPT, from, to, body });
|
|
781
|
+
}
|
|
782
|
+
function createCohortReadyMessage(fields) {
|
|
783
|
+
const { from, to, ...body } = fields;
|
|
784
|
+
return new BaseMessage({ type: COHORT_READY, from, to, body });
|
|
785
|
+
}
|
|
786
|
+
function createDistributeAggregatedDataMessage(fields) {
|
|
787
|
+
const { from, to, ...body } = fields;
|
|
788
|
+
return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from, to, body });
|
|
789
|
+
}
|
|
790
|
+
function createAuthorizationRequestMessage(fields) {
|
|
791
|
+
const { from, to, ...body } = fields;
|
|
792
|
+
return new BaseMessage({ type: AUTHORIZATION_REQUEST, from, to, body });
|
|
793
|
+
}
|
|
794
|
+
function createAggregatedNonceMessage(fields) {
|
|
795
|
+
const { from, to, ...body } = fields;
|
|
796
|
+
return new BaseMessage({ type: AGGREGATED_NONCE, from, to, body });
|
|
797
|
+
}
|
|
798
|
+
function createFallbackAuthorizationRequestMessage(fields) {
|
|
799
|
+
const { from, to, ...body } = fields;
|
|
800
|
+
return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from, to, body });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/core/signing-session.ts
|
|
804
|
+
var import_keypair = require("@did-btcr2/keypair");
|
|
805
|
+
var import_btc_signer4 = require("@scure/btc-signer");
|
|
806
|
+
var musig2 = __toESM(require("@scure/btc-signer/musig2"), 1);
|
|
807
|
+
var BeaconSigningSession = class {
|
|
808
|
+
/** Unique identifier for this signing session. */
|
|
809
|
+
id;
|
|
810
|
+
/** The cohort this session signs for. */
|
|
811
|
+
cohort;
|
|
812
|
+
/** The Bitcoin transaction being signed. */
|
|
813
|
+
pendingTx;
|
|
814
|
+
/** Previous output scripts for Taproot sighash computation (BIP-341). */
|
|
815
|
+
prevOutScripts;
|
|
816
|
+
/** Previous output values for Taproot sighash computation. */
|
|
817
|
+
prevOutValues;
|
|
818
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
819
|
+
nonceContributions = /* @__PURE__ */ new Map();
|
|
820
|
+
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
821
|
+
aggregatedNonce;
|
|
822
|
+
/** Map of participant DID to partial signature. */
|
|
823
|
+
partialSignatures = /* @__PURE__ */ new Map();
|
|
824
|
+
/** Final 64-byte Schnorr signature. */
|
|
825
|
+
signature;
|
|
826
|
+
/** Current signing session phase. */
|
|
827
|
+
phase;
|
|
828
|
+
/**
|
|
829
|
+
* Participant's MuSig2 secret nonce, held only on the participant side
|
|
830
|
+
* between {@link generateNonceContribution} and {@link generatePartialSignature}.
|
|
831
|
+
* Private and cleared on every terminal path (success, failure, or teardown
|
|
832
|
+
* via {@link clearSecrets}) so a spent or abandoned nonce cannot be reused or
|
|
833
|
+
* serialized.
|
|
834
|
+
*/
|
|
835
|
+
#secretNonce;
|
|
836
|
+
constructor({ id, cohort, pendingTx, prevOutScripts, prevOutValues }) {
|
|
837
|
+
this.id = id || crypto.randomUUID();
|
|
838
|
+
this.cohort = cohort;
|
|
839
|
+
this.pendingTx = pendingTx;
|
|
840
|
+
this.prevOutScripts = prevOutScripts || [];
|
|
841
|
+
this.prevOutValues = prevOutValues || [];
|
|
842
|
+
this.phase = "AwaitingNonceContributions" /* AwaitingNonceContributions */;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Computes the Taproot sighash (BIP-341) for the first input.
|
|
846
|
+
*/
|
|
847
|
+
get sigHash() {
|
|
848
|
+
if (!this.prevOutScripts.length || !this.prevOutValues.length) {
|
|
849
|
+
throw new SigningSessionError(
|
|
850
|
+
"Cannot compute sighash: missing prevOutScripts or prevOutValues.",
|
|
851
|
+
"SIGHASH_ERROR"
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return this.pendingTx.preimageWitnessV1(
|
|
855
|
+
0,
|
|
856
|
+
this.prevOutScripts,
|
|
857
|
+
import_btc_signer4.SigHash.DEFAULT,
|
|
858
|
+
this.prevOutValues
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
addNonceContribution(participantDid, nonceContribution) {
|
|
862
|
+
if (this.phase !== "AwaitingNonceContributions" /* AwaitingNonceContributions */) {
|
|
863
|
+
throw new SigningSessionError(
|
|
864
|
+
`Nonce contributions not expected. Current phase: ${this.phase}`,
|
|
865
|
+
"INVALID_PHASE",
|
|
866
|
+
{ phase: this.phase }
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
870
|
+
throw new SigningSessionError(
|
|
871
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
872
|
+
"UNKNOWN_PARTICIPANT",
|
|
873
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (nonceContribution.length !== 66) {
|
|
877
|
+
throw new SigningSessionError(
|
|
878
|
+
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
879
|
+
"INVALID_NONCE_LENGTH"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
if (this.nonceContributions.has(participantDid)) {
|
|
883
|
+
throw new SigningSessionError(
|
|
884
|
+
`Duplicate nonce contribution from ${participantDid}.`,
|
|
885
|
+
"DUPLICATE_NONCE"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
this.nonceContributions.set(participantDid, nonceContribution);
|
|
889
|
+
if (this.nonceContributions.size === this.cohort.participants.length) {
|
|
890
|
+
this.phase = "NonceContributionsReceived" /* NonceContributionsReceived */;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
generateAggregatedNonce() {
|
|
894
|
+
if (this.phase !== "NonceContributionsReceived" /* NonceContributionsReceived */) {
|
|
895
|
+
throw new SigningSessionError(
|
|
896
|
+
`Cannot aggregate nonces: phase is ${this.phase}, expected NonceContributionsReceived.`,
|
|
897
|
+
"INVALID_PHASE"
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
this.aggregatedNonce = musig2.nonceAggregate([...this.nonceContributions.values()]);
|
|
901
|
+
this.phase = "AwaitingPartialSignatures" /* AwaitingPartialSignatures */;
|
|
902
|
+
return this.aggregatedNonce;
|
|
903
|
+
}
|
|
904
|
+
addPartialSignature(participantDid, partialSig) {
|
|
905
|
+
if (this.phase !== "AwaitingPartialSignatures" /* AwaitingPartialSignatures */) {
|
|
906
|
+
throw new SigningSessionError(
|
|
907
|
+
`Partial signatures not expected. Current phase: ${this.phase}`,
|
|
908
|
+
"INVALID_PHASE"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (!this.cohort.participants.includes(participantDid)) {
|
|
912
|
+
throw new SigningSessionError(
|
|
913
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
914
|
+
"UNKNOWN_PARTICIPANT",
|
|
915
|
+
{ cohortId: this.cohort.id, participantDid }
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (this.partialSignatures.has(participantDid)) {
|
|
919
|
+
throw new SigningSessionError(
|
|
920
|
+
`Duplicate partial signature from ${participantDid}.`,
|
|
921
|
+
"DUPLICATE_PARTIAL_SIG"
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
this.partialSignatures.set(participantDid, partialSig);
|
|
925
|
+
if (this.partialSignatures.size === this.cohort.participants.length) {
|
|
926
|
+
this.phase = "PartialSignaturesReceived" /* PartialSignaturesReceived */;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
generateFinalSignature() {
|
|
930
|
+
if (this.phase !== "PartialSignaturesReceived" /* PartialSignaturesReceived */) {
|
|
931
|
+
throw new SigningSessionError(
|
|
932
|
+
`Cannot generate final signature: phase is ${this.phase}.`,
|
|
933
|
+
"INVALID_PHASE"
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if (!this.aggregatedNonce) {
|
|
937
|
+
throw new SigningSessionError("Aggregated nonce missing.", "MISSING_AGGREGATED_NONCE");
|
|
938
|
+
}
|
|
939
|
+
const session = new musig2.Session(
|
|
940
|
+
this.aggregatedNonce,
|
|
941
|
+
this.cohort.cohortKeys,
|
|
942
|
+
this.sigHash,
|
|
943
|
+
[this.cohort.tapTweak],
|
|
944
|
+
[true]
|
|
945
|
+
);
|
|
946
|
+
const pubNoncesByIndex = new Array(this.cohort.cohortKeys.length);
|
|
947
|
+
for (const [did, nonce] of this.nonceContributions) {
|
|
948
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
949
|
+
if (idx < 0) {
|
|
950
|
+
throw new SigningSessionError(
|
|
951
|
+
`Cannot verify nonce from ${did}: participant key missing from cohort.`,
|
|
952
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
953
|
+
{ participantDid: did }
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
pubNoncesByIndex[idx] = nonce;
|
|
957
|
+
}
|
|
958
|
+
for (const [did, partialSig] of this.partialSignatures) {
|
|
959
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
960
|
+
if (idx < 0) {
|
|
961
|
+
throw new SigningSessionError(
|
|
962
|
+
`Cannot verify partial signature from ${did}: participant key missing from cohort.`,
|
|
963
|
+
"UNKNOWN_PARTICIPANT_KEY",
|
|
964
|
+
{ participantDid: did }
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
|
|
968
|
+
if (!ok) {
|
|
969
|
+
throw new SigningSessionError(
|
|
970
|
+
`Bad partial signature from ${did}.`,
|
|
971
|
+
"BAD_PARTIAL_SIG",
|
|
972
|
+
{ participantDid: did, index: idx }
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
977
|
+
this.phase = "Complete" /* Complete */;
|
|
978
|
+
return this.signature;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Generates a fresh MuSig2 nonce contribution for the participant.
|
|
982
|
+
* Stores the secret nonce internally for use in `generatePartialSignature()`.
|
|
983
|
+
*/
|
|
984
|
+
generateNonceContribution(participantPublicKey, participantSecretKey) {
|
|
985
|
+
const aggPublicKey = musig2.keyAggExport(musig2.keyAggregate(this.cohort.cohortKeys));
|
|
986
|
+
const nonces = musig2.nonceGen(participantPublicKey, participantSecretKey, aggPublicKey);
|
|
987
|
+
this.#secretNonce = nonces.secret;
|
|
988
|
+
return nonces.public;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
992
|
+
* Requires the aggregated nonce to have been set first (via the service).
|
|
993
|
+
*
|
|
994
|
+
* Clears the stored secret nonce after use on every path (success or throw)
|
|
995
|
+
* via {@link clearSecrets}. JS cannot truly erase memory (GC may relocate
|
|
996
|
+
* buffers), but overwriting the bytes shortens the exposure window and
|
|
997
|
+
* prevents accidental reuse or serialization of a spent nonce - reuse of a
|
|
998
|
+
* MuSig2 nonce leaks the secret key.
|
|
999
|
+
*/
|
|
1000
|
+
generatePartialSignature(participantSecretKey) {
|
|
1001
|
+
if (!this.aggregatedNonce) {
|
|
1002
|
+
throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
|
|
1003
|
+
}
|
|
1004
|
+
if (!this.#secretNonce) {
|
|
1005
|
+
throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
|
|
1006
|
+
}
|
|
1007
|
+
const session = new musig2.Session(
|
|
1008
|
+
this.aggregatedNonce,
|
|
1009
|
+
this.cohort.cohortKeys,
|
|
1010
|
+
this.sigHash,
|
|
1011
|
+
[this.cohort.tapTweak],
|
|
1012
|
+
[true]
|
|
1013
|
+
);
|
|
1014
|
+
try {
|
|
1015
|
+
return session.sign(this.#secretNonce, participantSecretKey);
|
|
1016
|
+
} finally {
|
|
1017
|
+
this.clearSecrets();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Zeroize any retained secret nonce. Safe to call repeatedly and on any path
|
|
1022
|
+
* (completion, failure, or teardown of an abandoned session). Callers that
|
|
1023
|
+
* drop a session before it reaches a partial signature should invoke this so
|
|
1024
|
+
* the secret nonce does not linger on the live object.
|
|
1025
|
+
*/
|
|
1026
|
+
clearSecrets() {
|
|
1027
|
+
if (this.#secretNonce) {
|
|
1028
|
+
(0, import_keypair.wipe)(this.#secretNonce);
|
|
1029
|
+
this.#secretNonce = void 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
isComplete() {
|
|
1033
|
+
return this.phase === "Complete" /* Complete */;
|
|
1034
|
+
}
|
|
1035
|
+
isFailed() {
|
|
1036
|
+
return this.phase === "Failed" /* Failed */;
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/service/service.ts
|
|
1041
|
+
var DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
|
|
1042
|
+
var AggregationService = class {
|
|
1043
|
+
did;
|
|
1044
|
+
publicKey;
|
|
1045
|
+
maxUpdateSizeBytes;
|
|
1046
|
+
/** Per-cohort state, keyed by cohortId. */
|
|
1047
|
+
#cohortStates = /* @__PURE__ */ new Map();
|
|
1048
|
+
constructor({ did, publicKey, maxUpdateSizeBytes }) {
|
|
1049
|
+
this.did = did;
|
|
1050
|
+
this.publicKey = publicKey;
|
|
1051
|
+
this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
|
|
1052
|
+
}
|
|
1053
|
+
receive(message) {
|
|
1054
|
+
const version = message.version;
|
|
1055
|
+
if (version === void 0 || version !== AGGREGATION_WIRE_VERSION) {
|
|
1056
|
+
const cohortId = message.body?.cohortId;
|
|
1057
|
+
const state = cohortId ? this.#cohortStates.get(cohortId) : void 0;
|
|
1058
|
+
if (state) {
|
|
1059
|
+
state.rejections.push({
|
|
1060
|
+
from: message.from,
|
|
1061
|
+
code: "WRONG_VERSION",
|
|
1062
|
+
reason: `Expected wire version ${AGGREGATION_WIRE_VERSION}, got ${String(version)}`
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const type = message.type;
|
|
1068
|
+
switch (type) {
|
|
1069
|
+
case COHORT_OPT_IN:
|
|
1070
|
+
this.#handleOptIn(message);
|
|
1071
|
+
break;
|
|
1072
|
+
case SUBMIT_UPDATE:
|
|
1073
|
+
this.#handleSubmitUpdate(message);
|
|
1074
|
+
break;
|
|
1075
|
+
case SUBMIT_NONINCLUDED:
|
|
1076
|
+
this.#handleSubmitNonInclusion(message);
|
|
1077
|
+
break;
|
|
1078
|
+
case VALIDATION_ACK:
|
|
1079
|
+
this.#handleValidationAck(message);
|
|
1080
|
+
break;
|
|
1081
|
+
case NONCE_CONTRIBUTION:
|
|
1082
|
+
this.#handleNonceContribution(message);
|
|
1083
|
+
break;
|
|
1084
|
+
case SIGNATURE_AUTHORIZATION:
|
|
1085
|
+
this.#handleSignatureAuthorization(message);
|
|
1086
|
+
break;
|
|
1087
|
+
case FALLBACK_SIGNATURE:
|
|
1088
|
+
this.#handleFallbackSignature(message);
|
|
1089
|
+
break;
|
|
1090
|
+
default:
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Drain the rejection log for a cohort. Used by runners to surface silent
|
|
1096
|
+
* drops (bad proof, oversized update, wrong version, etc.) as structured
|
|
1097
|
+
* events without breaking the sans-I/O state machine contract.
|
|
1098
|
+
*/
|
|
1099
|
+
drainRejections(cohortId) {
|
|
1100
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1101
|
+
if (!state) return [];
|
|
1102
|
+
const out = state.rejections;
|
|
1103
|
+
state.rejections = [];
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Create a new cohort with the given config. Returns the cohort ID.
|
|
1108
|
+
* Cohort starts in `Created` phase - call `advertise()` to broadcast.
|
|
1109
|
+
*/
|
|
1110
|
+
createCohort(config) {
|
|
1111
|
+
const problems = validateCohortConditions(config);
|
|
1112
|
+
if (problems.length > 0) {
|
|
1113
|
+
throw new AggregationServiceError(
|
|
1114
|
+
`Invalid cohort conditions: ${problems.join("; ")}`,
|
|
1115
|
+
"INVALID_COHORT_CONDITIONS",
|
|
1116
|
+
{ problems }
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
const cohort = new AggregationCohort({
|
|
1120
|
+
serviceDid: this.did,
|
|
1121
|
+
minParticipants: config.minParticipants,
|
|
1122
|
+
network: config.network,
|
|
1123
|
+
beaconType: config.beaconType,
|
|
1124
|
+
recoveryKey: (0, import_utils3.hexToBytes)(config.recoveryKey),
|
|
1125
|
+
recoverySequence: config.recoverySequence,
|
|
1126
|
+
fundingModel: config.fundingModel,
|
|
1127
|
+
fallbackThreshold: config.fallbackThreshold
|
|
1128
|
+
});
|
|
1129
|
+
this.#cohortStates.set(cohort.id, {
|
|
1130
|
+
phase: "Created" /* Created */,
|
|
1131
|
+
cohort,
|
|
1132
|
+
config,
|
|
1133
|
+
pendingOptIns: /* @__PURE__ */ new Map(),
|
|
1134
|
+
acceptedParticipants: /* @__PURE__ */ new Set(),
|
|
1135
|
+
rejections: []
|
|
1136
|
+
});
|
|
1137
|
+
return cohort.id;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Advertise a cohort to discover participants.
|
|
1141
|
+
* Returns the advert message to broadcast.
|
|
1142
|
+
*/
|
|
1143
|
+
advertise(cohortId) {
|
|
1144
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1145
|
+
if (!state) {
|
|
1146
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1147
|
+
}
|
|
1148
|
+
if (state.phase !== "Created" /* Created */) {
|
|
1149
|
+
throw new AggregationServiceError(
|
|
1150
|
+
`Cannot advertise cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1151
|
+
"INVALID_PHASE",
|
|
1152
|
+
{ cohortId, phase: state.phase }
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
const { network, ...conditions } = state.config;
|
|
1156
|
+
const message = createCohortAdvertMessage({
|
|
1157
|
+
from: this.did,
|
|
1158
|
+
cohortId,
|
|
1159
|
+
network,
|
|
1160
|
+
communicationPk: this.publicKey.compressed,
|
|
1161
|
+
...conditions
|
|
1162
|
+
});
|
|
1163
|
+
state.phase = "Advertised" /* Advertised */;
|
|
1164
|
+
return [message];
|
|
1165
|
+
}
|
|
1166
|
+
/** Pending opt-ins awaiting operator approval. */
|
|
1167
|
+
pendingOptIns(cohortId) {
|
|
1168
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1169
|
+
if (!state) return /* @__PURE__ */ new Map();
|
|
1170
|
+
const map = /* @__PURE__ */ new Map();
|
|
1171
|
+
for (const [did, optIn] of state.pendingOptIns) {
|
|
1172
|
+
if (!state.acceptedParticipants.has(did)) {
|
|
1173
|
+
map.set(did, optIn);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return map;
|
|
1177
|
+
}
|
|
1178
|
+
#handleOptIn(message) {
|
|
1179
|
+
const cohortId = message.body?.cohortId;
|
|
1180
|
+
if (!cohortId) return;
|
|
1181
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1182
|
+
if (!state) return;
|
|
1183
|
+
if (state.phase !== "Advertised" /* Advertised */) return;
|
|
1184
|
+
const participantDid = message.from;
|
|
1185
|
+
const participantPk = message.body?.participantPk;
|
|
1186
|
+
const communicationPk = message.body?.communicationPk;
|
|
1187
|
+
if (!participantPk || !communicationPk) return;
|
|
1188
|
+
if (state.acceptedParticipants.has(participantDid)) return;
|
|
1189
|
+
state.pendingOptIns.set(participantDid, {
|
|
1190
|
+
cohortId,
|
|
1191
|
+
participantDid,
|
|
1192
|
+
participantPk,
|
|
1193
|
+
communicationPk
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Service operator accepts a participant's opt-in.
|
|
1198
|
+
* Returns the accept message to send.
|
|
1199
|
+
*/
|
|
1200
|
+
acceptParticipant(cohortId, participantDid) {
|
|
1201
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1202
|
+
if (!state) {
|
|
1203
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1204
|
+
}
|
|
1205
|
+
const optIn = state.pendingOptIns.get(participantDid);
|
|
1206
|
+
if (!optIn) {
|
|
1207
|
+
throw new AggregationServiceError(
|
|
1208
|
+
`No pending opt-in from ${participantDid} for cohort ${cohortId}.`,
|
|
1209
|
+
"NO_OPT_IN",
|
|
1210
|
+
{ cohortId, participantDid }
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
if (state.acceptedParticipants.has(participantDid)) {
|
|
1214
|
+
throw new AggregationServiceError(
|
|
1215
|
+
`Participant ${participantDid} already accepted into cohort ${cohortId}.`,
|
|
1216
|
+
"ALREADY_ACCEPTED",
|
|
1217
|
+
{ cohortId, participantDid }
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
const maxParticipants = state.config.maxParticipants;
|
|
1221
|
+
if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
|
|
1222
|
+
throw new AggregationServiceError(
|
|
1223
|
+
`Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
|
|
1224
|
+
"COHORT_FULL",
|
|
1225
|
+
{ cohortId, maxParticipants }
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
state.acceptedParticipants.add(participantDid);
|
|
1229
|
+
state.cohort.participants.push(participantDid);
|
|
1230
|
+
state.cohort.participantKeys.set(participantDid, optIn.participantPk);
|
|
1231
|
+
state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
|
|
1232
|
+
return [createCohortOptInAcceptMessage({
|
|
1233
|
+
from: this.did,
|
|
1234
|
+
to: participantDid,
|
|
1235
|
+
cohortId
|
|
1236
|
+
})];
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Finalize cohort keygen: compute MuSig2 Taproot beacon address and send
|
|
1240
|
+
* COHORT_READY messages to all accepted participants.
|
|
1241
|
+
*/
|
|
1242
|
+
finalizeKeygen(cohortId) {
|
|
1243
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1244
|
+
if (!state) {
|
|
1245
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1246
|
+
}
|
|
1247
|
+
if (state.phase !== "Advertised" /* Advertised */) {
|
|
1248
|
+
throw new AggregationServiceError(
|
|
1249
|
+
`Cannot finalize keygen for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1250
|
+
"INVALID_PHASE",
|
|
1251
|
+
{ cohortId, phase: state.phase }
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (state.acceptedParticipants.size < state.config.minParticipants) {
|
|
1255
|
+
throw new AggregationServiceError(
|
|
1256
|
+
`Cohort ${cohortId} has only ${state.acceptedParticipants.size} accepted participants, need ${state.config.minParticipants}.`,
|
|
1257
|
+
"NOT_ENOUGH_PARTICIPANTS",
|
|
1258
|
+
{ cohortId }
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
const maxParticipants = state.config.maxParticipants;
|
|
1262
|
+
if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
|
|
1263
|
+
throw new AggregationServiceError(
|
|
1264
|
+
`Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
|
|
1265
|
+
"TOO_MANY_PARTICIPANTS",
|
|
1266
|
+
{ cohortId, maxParticipants }
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const beaconAddress = state.cohort.computeBeaconAddress();
|
|
1270
|
+
state.phase = "CohortSet" /* CohortSet */;
|
|
1271
|
+
const messages = [];
|
|
1272
|
+
for (const participantDid of state.cohort.participants) {
|
|
1273
|
+
messages.push(createCohortReadyMessage({
|
|
1274
|
+
from: this.did,
|
|
1275
|
+
to: participantDid,
|
|
1276
|
+
cohortId,
|
|
1277
|
+
beaconAddress,
|
|
1278
|
+
cohortKeys: state.cohort.cohortKeys
|
|
1279
|
+
}));
|
|
1280
|
+
}
|
|
1281
|
+
return messages;
|
|
1282
|
+
}
|
|
1283
|
+
/** Updates collected so far for a cohort. */
|
|
1284
|
+
collectedUpdates(cohortId) {
|
|
1285
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1286
|
+
if (!state) return /* @__PURE__ */ new Map();
|
|
1287
|
+
return state.cohort.pendingUpdates;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Handle an incoming SUBMIT_UPDATE message from a participant containing their signed update to
|
|
1291
|
+
* submit for aggregation.
|
|
1292
|
+
* @param {BaseMessage} message - incoming SUBMIT_UPDATE message containing a participant's signed
|
|
1293
|
+
* update to submit for aggregation
|
|
1294
|
+
* @returns {void} - no return value; updates the service state with the submitted update if valid
|
|
1295
|
+
*/
|
|
1296
|
+
#handleSubmitUpdate(message) {
|
|
1297
|
+
const cohortId = message.body?.cohortId;
|
|
1298
|
+
if (!cohortId) return;
|
|
1299
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1300
|
+
if (!state) return;
|
|
1301
|
+
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
1302
|
+
const signedUpdate = message.body?.signedUpdate;
|
|
1303
|
+
if (!signedUpdate) {
|
|
1304
|
+
state.rejections.push({
|
|
1305
|
+
from: message.from,
|
|
1306
|
+
code: "UPDATE_MALFORMED",
|
|
1307
|
+
reason: "SUBMIT_UPDATE missing signedUpdate body"
|
|
1308
|
+
});
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const canonicalSize = (0, import_common4.canonicalize)(signedUpdate).length;
|
|
1312
|
+
if (canonicalSize > this.maxUpdateSizeBytes) {
|
|
1313
|
+
state.rejections.push({
|
|
1314
|
+
from: message.from,
|
|
1315
|
+
code: "UPDATE_TOO_LARGE",
|
|
1316
|
+
reason: `Canonicalized update is ${canonicalSize} bytes; max allowed is ${this.maxUpdateSizeBytes}`
|
|
1317
|
+
});
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (!this.#verifySubmittedUpdate(state, message.from, signedUpdate)) {
|
|
1321
|
+
state.rejections.push({
|
|
1322
|
+
from: message.from,
|
|
1323
|
+
code: "UPDATE_VERIFICATION_FAILED",
|
|
1324
|
+
reason: "BIP-340 Data Integrity proof verification failed"
|
|
1325
|
+
});
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
if (state.cohort.nonIncluded.has(message.from)) {
|
|
1329
|
+
state.rejections.push({
|
|
1330
|
+
from: message.from,
|
|
1331
|
+
code: "UPDATE_MALFORMED",
|
|
1332
|
+
reason: "Participant already declined this round; cannot also submit an update"
|
|
1333
|
+
});
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (state.cohort.pendingUpdates.has(message.from)) {
|
|
1337
|
+
state.rejections.push({
|
|
1338
|
+
from: message.from,
|
|
1339
|
+
code: "UPDATE_MALFORMED",
|
|
1340
|
+
reason: "Participant already submitted an update this round; cannot resubmit"
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
state.cohort.addUpdate(message.from, signedUpdate);
|
|
1345
|
+
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
1346
|
+
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
1347
|
+
}
|
|
1348
|
+
if (state.cohort.hasAllResponses()) {
|
|
1349
|
+
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
|
|
1354
|
+
* update this round (cooperative non-inclusion). Membership is proven by the
|
|
1355
|
+
* signed transport envelope, so the body carries only the cohortId. The member
|
|
1356
|
+
* stays in the cohort and still signs; it is absent from the CAS map and
|
|
1357
|
+
* carries a non-inclusion leaf in the SMT.
|
|
1358
|
+
*/
|
|
1359
|
+
#handleSubmitNonInclusion(message) {
|
|
1360
|
+
const cohortId = message.body?.cohortId;
|
|
1361
|
+
if (!cohortId) return;
|
|
1362
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1363
|
+
if (!state) return;
|
|
1364
|
+
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
1365
|
+
if (!state.cohort.participants.includes(message.from)) {
|
|
1366
|
+
state.rejections.push({
|
|
1367
|
+
from: message.from,
|
|
1368
|
+
code: "UPDATE_MALFORMED",
|
|
1369
|
+
reason: "Sender is not a member of this cohort"
|
|
1370
|
+
});
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (state.cohort.pendingUpdates.has(message.from) || state.cohort.nonIncluded.has(message.from)) {
|
|
1374
|
+
state.rejections.push({
|
|
1375
|
+
from: message.from,
|
|
1376
|
+
code: "UPDATE_MALFORMED",
|
|
1377
|
+
reason: "Participant already responded this round"
|
|
1378
|
+
});
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
state.cohort.addNonInclusion(message.from);
|
|
1382
|
+
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
1383
|
+
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
1384
|
+
}
|
|
1385
|
+
if (state.cohort.hasAllResponses()) {
|
|
1386
|
+
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Verify the BIP-340 Schnorr Data Integrity proof on a submitted update using the
|
|
1391
|
+
* participant's public key from their cohort opt-in. Returns `false` (and the
|
|
1392
|
+
* update is silently dropped) if the proof is missing, the verificationMethod does
|
|
1393
|
+
* not name the sender's DID, the participant has no opt-in on record, or the
|
|
1394
|
+
* signature fails verification.
|
|
1395
|
+
* @param {ServiceCohortState} state - the current state of the cohort to which the update was submitted
|
|
1396
|
+
* @param {string} sender - the DID of the participant who submitted the update
|
|
1397
|
+
* @param {SecuredDocument} signedUpdate - the signed update containing the proof to verify
|
|
1398
|
+
* @returns {boolean} - `true` if the proof is valid and the update can be accepted; `false` otherwise
|
|
1399
|
+
*/
|
|
1400
|
+
#verifySubmittedUpdate(state, sender, signedUpdate) {
|
|
1401
|
+
const proof = signedUpdate.proof;
|
|
1402
|
+
if (!proof?.verificationMethod || !proof.proofValue) return false;
|
|
1403
|
+
const vmDid = proof.verificationMethod.split("#")[0];
|
|
1404
|
+
if (vmDid !== sender) return false;
|
|
1405
|
+
const optIn = state.pendingOptIns.get(sender);
|
|
1406
|
+
if (!optIn) return false;
|
|
1407
|
+
try {
|
|
1408
|
+
const multikey = import_cryptosuite.SchnorrMultikey.fromPublicKey({
|
|
1409
|
+
id: proof.verificationMethod,
|
|
1410
|
+
controller: sender,
|
|
1411
|
+
publicKeyBytes: optIn.participantPk
|
|
1412
|
+
});
|
|
1413
|
+
const suite = new import_cryptosuite.BIP340Cryptosuite(multikey);
|
|
1414
|
+
return suite.verifyProof(signedUpdate).verified === true;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Build the aggregated data structure (CAS Announcement or SMT tree) and
|
|
1421
|
+
* return distribute messages to send to all participants for validation.
|
|
1422
|
+
*/
|
|
1423
|
+
buildAndDistribute(cohortId) {
|
|
1424
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1425
|
+
if (!state) {
|
|
1426
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1427
|
+
}
|
|
1428
|
+
if (state.phase !== "UpdatesCollected" /* UpdatesCollected */) {
|
|
1429
|
+
throw new AggregationServiceError(
|
|
1430
|
+
`Cannot build aggregated data for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1431
|
+
"INVALID_PHASE",
|
|
1432
|
+
{ cohortId, phase: state.phase }
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
const strategy = getBeaconStrategy(state.config.beaconType);
|
|
1436
|
+
if (!strategy) {
|
|
1437
|
+
throw new AggregationServiceError(
|
|
1438
|
+
`Unsupported beacon type: ${state.config.beaconType}`,
|
|
1439
|
+
"UNSUPPORTED_BEACON_TYPE",
|
|
1440
|
+
{ cohortId, beaconType: state.config.beaconType }
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
strategy.buildAggregatedData(state.cohort);
|
|
1444
|
+
const signalBytesHex = (0, import_utils3.bytesToHex)(state.cohort.signalBytes);
|
|
1445
|
+
state.phase = "DataDistributed" /* DataDistributed */;
|
|
1446
|
+
const messages = [];
|
|
1447
|
+
for (const participantDid of state.cohort.participants) {
|
|
1448
|
+
const payload = strategy.getDistributePayload(state.cohort, participantDid);
|
|
1449
|
+
messages.push(createDistributeAggregatedDataMessage({
|
|
1450
|
+
from: this.did,
|
|
1451
|
+
to: participantDid,
|
|
1452
|
+
cohortId,
|
|
1453
|
+
beaconType: state.config.beaconType,
|
|
1454
|
+
signalBytesHex,
|
|
1455
|
+
casAnnouncement: payload.casAnnouncement,
|
|
1456
|
+
smtProof: payload.smtProof
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
return messages;
|
|
1460
|
+
}
|
|
1461
|
+
validationProgress(cohortId) {
|
|
1462
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1463
|
+
if (!state) {
|
|
1464
|
+
return { approved: /* @__PURE__ */ new Set(), rejected: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Set(), total: 0 };
|
|
1465
|
+
}
|
|
1466
|
+
const approved = state.cohort.validationAcks;
|
|
1467
|
+
const rejected = state.cohort.validationRejections;
|
|
1468
|
+
const allParticipants = new Set(state.cohort.participants);
|
|
1469
|
+
const responded = /* @__PURE__ */ new Set([...approved, ...rejected]);
|
|
1470
|
+
const pending = new Set([...allParticipants].filter((p) => !responded.has(p)));
|
|
1471
|
+
return {
|
|
1472
|
+
approved,
|
|
1473
|
+
rejected,
|
|
1474
|
+
pending,
|
|
1475
|
+
total: allParticipants.size
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
#handleValidationAck(message) {
|
|
1479
|
+
const cohortId = message.body?.cohortId;
|
|
1480
|
+
if (!cohortId) return;
|
|
1481
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1482
|
+
if (!state) return;
|
|
1483
|
+
if (state.phase !== "DataDistributed" /* DataDistributed */) return;
|
|
1484
|
+
const approved = message.body?.approved;
|
|
1485
|
+
if (approved === void 0) return;
|
|
1486
|
+
state.cohort.addValidation(message.from, approved);
|
|
1487
|
+
if (state.cohort.isFullyValidated()) {
|
|
1488
|
+
state.phase = "Validated" /* Validated */;
|
|
1489
|
+
} else if (state.cohort.hasAllValidationResponses()) {
|
|
1490
|
+
state.phase = "Failed" /* Failed */;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Start a signing session by creating auth requests for all participants.
|
|
1495
|
+
* The caller provides the transaction data - typically built via
|
|
1496
|
+
* `buildBeaconTransaction()` against a Bitcoin connection.
|
|
1497
|
+
*/
|
|
1498
|
+
startSigning(cohortId, txData) {
|
|
1499
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1500
|
+
if (!state) {
|
|
1501
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1502
|
+
}
|
|
1503
|
+
if (state.phase !== "Validated" /* Validated */) {
|
|
1504
|
+
throw new AggregationServiceError(
|
|
1505
|
+
`Cannot start signing for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1506
|
+
"INVALID_PHASE",
|
|
1507
|
+
{ cohortId, phase: state.phase }
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
const session = new BeaconSigningSession({
|
|
1511
|
+
cohort: state.cohort,
|
|
1512
|
+
pendingTx: txData.tx,
|
|
1513
|
+
prevOutScripts: txData.prevOutScripts,
|
|
1514
|
+
prevOutValues: txData.prevOutValues
|
|
1515
|
+
});
|
|
1516
|
+
state.signingSession = session;
|
|
1517
|
+
state.phase = "SigningStarted" /* SigningStarted */;
|
|
1518
|
+
const prevOutScript = txData.prevOutScripts[0];
|
|
1519
|
+
if (!prevOutScript) {
|
|
1520
|
+
throw new AggregationServiceError(
|
|
1521
|
+
`Cannot start signing for cohort ${cohortId}: txData.prevOutScripts[0] is missing.`,
|
|
1522
|
+
"MISSING_PREV_OUT_SCRIPT",
|
|
1523
|
+
{ cohortId }
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const messages = [];
|
|
1527
|
+
for (const participantDid of state.cohort.participants) {
|
|
1528
|
+
messages.push(createAuthorizationRequestMessage({
|
|
1529
|
+
from: this.did,
|
|
1530
|
+
to: participantDid,
|
|
1531
|
+
cohortId,
|
|
1532
|
+
sessionId: session.id,
|
|
1533
|
+
pendingTx: txData.tx.hex,
|
|
1534
|
+
prevOutScriptHex: (0, import_utils3.bytesToHex)(prevOutScript),
|
|
1535
|
+
prevOutValue: txData.prevOutValues[0]?.toString() ?? "0"
|
|
1536
|
+
}));
|
|
1537
|
+
}
|
|
1538
|
+
return messages;
|
|
1539
|
+
}
|
|
1540
|
+
#handleNonceContribution(message) {
|
|
1541
|
+
const cohortId = message.body?.cohortId;
|
|
1542
|
+
if (!cohortId) return;
|
|
1543
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1544
|
+
if (!state || !state.signingSession) return;
|
|
1545
|
+
if (state.phase !== "SigningStarted" /* SigningStarted */) return;
|
|
1546
|
+
const sessionId = message.body?.sessionId;
|
|
1547
|
+
if (sessionId !== state.signingSession.id) return;
|
|
1548
|
+
const nonceContribution = message.body?.nonceContribution;
|
|
1549
|
+
if (!nonceContribution) return;
|
|
1550
|
+
state.signingSession.addNonceContribution(message.from, nonceContribution);
|
|
1551
|
+
if (state.signingSession.nonceContributions.size === state.cohort.participants.length) {
|
|
1552
|
+
state.phase = "NoncesCollected" /* NoncesCollected */;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Generate the aggregated nonce and return messages to send to participants.
|
|
1557
|
+
* Call after `validationProgress(cohortId).approved.size === total`.
|
|
1558
|
+
*/
|
|
1559
|
+
sendAggregatedNonce(cohortId) {
|
|
1560
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1561
|
+
if (!state) {
|
|
1562
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1563
|
+
}
|
|
1564
|
+
if (state.phase !== "NoncesCollected" /* NoncesCollected */ || !state.signingSession) {
|
|
1565
|
+
throw new AggregationServiceError(
|
|
1566
|
+
`Cannot send aggregated nonce for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1567
|
+
"INVALID_PHASE",
|
|
1568
|
+
{ cohortId, phase: state.phase }
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
const aggregatedNonce = state.signingSession.generateAggregatedNonce();
|
|
1572
|
+
state.phase = "AwaitingPartialSigs" /* AwaitingPartialSigs */;
|
|
1573
|
+
const messages = [];
|
|
1574
|
+
for (const participantDid of state.cohort.participants) {
|
|
1575
|
+
messages.push(createAggregatedNonceMessage({
|
|
1576
|
+
from: this.did,
|
|
1577
|
+
to: participantDid,
|
|
1578
|
+
cohortId,
|
|
1579
|
+
sessionId: state.signingSession.id,
|
|
1580
|
+
aggregatedNonce
|
|
1581
|
+
}));
|
|
1582
|
+
}
|
|
1583
|
+
return messages;
|
|
1584
|
+
}
|
|
1585
|
+
#handleSignatureAuthorization(message) {
|
|
1586
|
+
const cohortId = message.body?.cohortId;
|
|
1587
|
+
if (!cohortId) return;
|
|
1588
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1589
|
+
if (!state || !state.signingSession) return;
|
|
1590
|
+
if (state.phase !== "AwaitingPartialSigs" /* AwaitingPartialSigs */) return;
|
|
1591
|
+
const sessionId = message.body?.sessionId;
|
|
1592
|
+
if (sessionId !== state.signingSession.id) return;
|
|
1593
|
+
const partialSignature = message.body?.partialSignature;
|
|
1594
|
+
if (!partialSignature) return;
|
|
1595
|
+
state.signingSession.addPartialSignature(message.from, partialSignature);
|
|
1596
|
+
if (state.signingSession.partialSignatures.size === state.cohort.participants.length) {
|
|
1597
|
+
const signature = state.signingSession.generateFinalSignature();
|
|
1598
|
+
state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
|
|
1599
|
+
state.result = {
|
|
1600
|
+
cohortId,
|
|
1601
|
+
signature,
|
|
1602
|
+
signedTx: state.signingSession.pendingTx,
|
|
1603
|
+
path: "key-path"
|
|
1604
|
+
};
|
|
1605
|
+
state.phase = "Complete" /* Complete */;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Abandon the optimistic n-of-n key path and ask members to authorize the
|
|
1610
|
+
* k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
|
|
1611
|
+
* liveness, ADR 042). Reuses the in-flight signing session's transaction and
|
|
1612
|
+
* spent output, so the announcement and its outputs are unchanged: only the
|
|
1613
|
+
* witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
|
|
1614
|
+
* participant.
|
|
1615
|
+
*
|
|
1616
|
+
* Callable once optimistic signing has started (the session and its tx exist)
|
|
1617
|
+
* and before it completes. A cohort can take exactly one of the two paths: the
|
|
1618
|
+
* caller (runner) must commit to fallback and stop driving the optimistic path.
|
|
1619
|
+
*/
|
|
1620
|
+
startFallbackSigning(cohortId) {
|
|
1621
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1622
|
+
if (!state) {
|
|
1623
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
1624
|
+
}
|
|
1625
|
+
if (!state.signingSession) {
|
|
1626
|
+
throw new AggregationServiceError(
|
|
1627
|
+
`Cannot start fallback for cohort ${cohortId}: no signing session.`,
|
|
1628
|
+
"NO_SIGNING_SESSION",
|
|
1629
|
+
{ cohortId }
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
const signingPhases = [
|
|
1633
|
+
"SigningStarted" /* SigningStarted */,
|
|
1634
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
1635
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
1636
|
+
];
|
|
1637
|
+
if (!signingPhases.includes(state.phase)) {
|
|
1638
|
+
throw new AggregationServiceError(
|
|
1639
|
+
`Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
1640
|
+
"INVALID_PHASE",
|
|
1641
|
+
{ cohortId, phase: state.phase }
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
const session = state.signingSession;
|
|
1645
|
+
const prevOutScript = session.prevOutScripts[0];
|
|
1646
|
+
const prevOutValue = session.prevOutValues[0];
|
|
1647
|
+
if (!prevOutScript || prevOutValue === void 0) {
|
|
1648
|
+
throw new AggregationServiceError(
|
|
1649
|
+
`Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
|
|
1650
|
+
"MISSING_PREV_OUT",
|
|
1651
|
+
{ cohortId }
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
1655
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
1656
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
1657
|
+
});
|
|
1658
|
+
state.fallbackSignatures = /* @__PURE__ */ new Map();
|
|
1659
|
+
state.phase = "FallbackRequested" /* FallbackRequested */;
|
|
1660
|
+
const messages = [];
|
|
1661
|
+
for (const participantDid of state.cohort.participants) {
|
|
1662
|
+
messages.push(createFallbackAuthorizationRequestMessage({
|
|
1663
|
+
from: this.did,
|
|
1664
|
+
to: participantDid,
|
|
1665
|
+
cohortId,
|
|
1666
|
+
sessionId: session.id,
|
|
1667
|
+
pendingTx: session.pendingTx.hex,
|
|
1668
|
+
prevOutScriptHex: (0, import_utils3.bytesToHex)(prevOutScript),
|
|
1669
|
+
prevOutValue: prevOutValue.toString(),
|
|
1670
|
+
fallbackLeafScriptHex: (0, import_utils3.bytesToHex)(fallbackLeaf)
|
|
1671
|
+
}));
|
|
1672
|
+
}
|
|
1673
|
+
return messages;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
|
|
1677
|
+
* signature over the fallback script-path sighash. The signature is
|
|
1678
|
+
* authenticated to the sender (its `signerPk` must be the sender's own cohort
|
|
1679
|
+
* key and the signature must verify against the sighash) and collected. Once k
|
|
1680
|
+
* valid signatures are in, the k-of-n fallback spend is assembled and the
|
|
1681
|
+
* cohort completes via the script path.
|
|
1682
|
+
*/
|
|
1683
|
+
#handleFallbackSignature(message) {
|
|
1684
|
+
const cohortId = message.body?.cohortId;
|
|
1685
|
+
if (!cohortId) return;
|
|
1686
|
+
const state = this.#cohortStates.get(cohortId);
|
|
1687
|
+
if (!state || !state.signingSession || !state.fallbackSignatures) return;
|
|
1688
|
+
if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
|
|
1689
|
+
const sessionId = message.body?.sessionId;
|
|
1690
|
+
if (sessionId !== state.signingSession.id) return;
|
|
1691
|
+
const signerPk = message.body?.signerPk;
|
|
1692
|
+
const fallbackSignature = message.body?.fallbackSignature;
|
|
1693
|
+
if (!signerPk || !fallbackSignature) return;
|
|
1694
|
+
const prevOutScript = state.signingSession.prevOutScripts[0];
|
|
1695
|
+
const prevOutValue = state.signingSession.prevOutValues[0];
|
|
1696
|
+
if (!prevOutScript || prevOutValue === void 0) return;
|
|
1697
|
+
const memberKey = state.cohort.participantKeys.get(message.from);
|
|
1698
|
+
if (!memberKey) {
|
|
1699
|
+
state.rejections.push({ from: message.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
const memberXOnly = memberKey.slice(1);
|
|
1703
|
+
if (signerPk.length !== 32 || !memberXOnly.every((b, i) => b === signerPk[i])) {
|
|
1704
|
+
state.rejections.push({ from: message.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
1708
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
1709
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
1710
|
+
});
|
|
1711
|
+
const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
1712
|
+
let valid = false;
|
|
1713
|
+
try {
|
|
1714
|
+
valid = fallbackSignature.length === 64 && import_secp256k14.schnorr.verify(fallbackSignature, sighash, signerPk);
|
|
1715
|
+
} catch {
|
|
1716
|
+
valid = false;
|
|
1717
|
+
}
|
|
1718
|
+
if (!valid) {
|
|
1719
|
+
state.rejections.push({ from: message.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
state.fallbackSignatures.set(message.from, { pubKey: signerPk, signature: fallbackSignature });
|
|
1723
|
+
if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
|
|
1724
|
+
const signedTx = buildFallbackSpend({
|
|
1725
|
+
pendingTx: state.signingSession.pendingTx,
|
|
1726
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
1727
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold,
|
|
1728
|
+
recoveryKey: state.cohort.recoveryKey,
|
|
1729
|
+
recoverySequence: state.cohort.recoverySequence,
|
|
1730
|
+
fundingModel: state.cohort.fundingModel,
|
|
1731
|
+
network: state.cohort.network,
|
|
1732
|
+
prevOutScript,
|
|
1733
|
+
prevOutValue,
|
|
1734
|
+
signatures: [...state.fallbackSignatures.values()]
|
|
1735
|
+
});
|
|
1736
|
+
state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
|
|
1737
|
+
state.phase = "Complete" /* Complete */;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
getResult(cohortId) {
|
|
1741
|
+
return this.#cohortStates.get(cohortId)?.result;
|
|
1742
|
+
}
|
|
1743
|
+
getCohortPhase(cohortId) {
|
|
1744
|
+
return this.#cohortStates.get(cohortId)?.phase;
|
|
1745
|
+
}
|
|
1746
|
+
getCohort(cohortId) {
|
|
1747
|
+
return this.#cohortStates.get(cohortId)?.cohort;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Get the signing session ID for a cohort, if a signing session has been started.
|
|
1751
|
+
* @param {string} cohortId - The cohort ID.
|
|
1752
|
+
* @returns {string | undefined} The session ID, or undefined if no session is active.
|
|
1753
|
+
*/
|
|
1754
|
+
getSigningSessionId(cohortId) {
|
|
1755
|
+
return this.#cohortStates.get(cohortId)?.signingSession?.id;
|
|
1756
|
+
}
|
|
1757
|
+
get cohorts() {
|
|
1758
|
+
return [...this.#cohortStates.values()].map((s) => s.cohort);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Remove a cohort from the state map. Used by runners to GC state on cohort
|
|
1762
|
+
* completion, failure, or expiry. No-op if the cohort doesn't exist.
|
|
1763
|
+
*/
|
|
1764
|
+
removeCohort(cohortId) {
|
|
1765
|
+
this.#cohortStates.delete(cohortId);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
// src/service/service-runner.ts
|
|
1770
|
+
var import_bitcoin3 = require("@did-btcr2/bitcoin");
|
|
1771
|
+
|
|
1772
|
+
// src/core/typed-emitter.ts
|
|
1773
|
+
var TypedEventEmitter = class {
|
|
1774
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
1775
|
+
/** Subscribe to an event. */
|
|
1776
|
+
on(event, listener) {
|
|
1777
|
+
let set = this.#listeners.get(event);
|
|
1778
|
+
if (!set) {
|
|
1779
|
+
set = /* @__PURE__ */ new Set();
|
|
1780
|
+
this.#listeners.set(event, set);
|
|
1781
|
+
}
|
|
1782
|
+
set.add(listener);
|
|
1783
|
+
return this;
|
|
1784
|
+
}
|
|
1785
|
+
/** Subscribe to an event, automatically unsubscribing after the first call. */
|
|
1786
|
+
once(event, listener) {
|
|
1787
|
+
const wrapped = ((...args) => {
|
|
1788
|
+
this.off(event, wrapped);
|
|
1789
|
+
listener(...args);
|
|
1790
|
+
});
|
|
1791
|
+
return this.on(event, wrapped);
|
|
1792
|
+
}
|
|
1793
|
+
/** Unsubscribe a specific listener. */
|
|
1794
|
+
off(event, listener) {
|
|
1795
|
+
this.#listeners.get(event)?.delete(listener);
|
|
1796
|
+
return this;
|
|
1797
|
+
}
|
|
1798
|
+
/** Emit an event to all subscribed listeners. */
|
|
1799
|
+
emit(event, ...args) {
|
|
1800
|
+
const set = this.#listeners.get(event);
|
|
1801
|
+
if (!set || set.size === 0) return false;
|
|
1802
|
+
for (const listener of set) {
|
|
1803
|
+
try {
|
|
1804
|
+
listener(...args);
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
console.error(`Listener for event "${String(event)}" threw:`, err);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
/** Remove all listeners (optionally for a specific event). */
|
|
1812
|
+
removeAllListeners(event) {
|
|
1813
|
+
if (event === void 0) {
|
|
1814
|
+
this.#listeners.clear();
|
|
1815
|
+
} else {
|
|
1816
|
+
this.#listeners.delete(event);
|
|
1817
|
+
}
|
|
1818
|
+
return this;
|
|
1819
|
+
}
|
|
1820
|
+
/** Number of listeners for an event. */
|
|
1821
|
+
listenerCount(event) {
|
|
1822
|
+
return this.#listeners.get(event)?.size ?? 0;
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/service/service-runner.ts
|
|
1827
|
+
var DEFAULT_FEE_ESTIMATOR = new import_bitcoin3.StaticFeeEstimator(5);
|
|
1828
|
+
var DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 6e4;
|
|
1829
|
+
var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEventEmitter {
|
|
1830
|
+
/** Direct access to the underlying state machine for advanced use. */
|
|
1831
|
+
session;
|
|
1832
|
+
#transport;
|
|
1833
|
+
#did;
|
|
1834
|
+
#defaultConfig;
|
|
1835
|
+
#onOptInReceived;
|
|
1836
|
+
#onReadyToFinalize;
|
|
1837
|
+
#onProvideTxData;
|
|
1838
|
+
#feeEstimator;
|
|
1839
|
+
#cohortTtlMs;
|
|
1840
|
+
#phaseTimeoutMs;
|
|
1841
|
+
#advertRepeatIntervalMs;
|
|
1842
|
+
#autoFallbackOnStall;
|
|
1843
|
+
/** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
|
|
1844
|
+
static #SIGNING_PHASES = [
|
|
1845
|
+
"SigningStarted" /* SigningStarted */,
|
|
1846
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
1847
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
1848
|
+
];
|
|
1849
|
+
/** Per-cohort run state, keyed by cohortId. */
|
|
1850
|
+
#contexts = /* @__PURE__ */ new Map();
|
|
1851
|
+
#handlersRegistered = false;
|
|
1852
|
+
#stopped = false;
|
|
1853
|
+
constructor(options) {
|
|
1854
|
+
super();
|
|
1855
|
+
this.#transport = options.transport;
|
|
1856
|
+
this.#did = options.did;
|
|
1857
|
+
this.#defaultConfig = options.config;
|
|
1858
|
+
this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
|
|
1859
|
+
this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
|
|
1860
|
+
finalize: acceptedCount >= minRequired
|
|
1861
|
+
}));
|
|
1862
|
+
this.#onProvideTxData = options.onProvideTxData;
|
|
1863
|
+
this.#feeEstimator = options.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
1864
|
+
this.#cohortTtlMs = options.cohortTtlMs;
|
|
1865
|
+
this.#phaseTimeoutMs = options.phaseTimeoutMs;
|
|
1866
|
+
this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
1867
|
+
this.#autoFallbackOnStall = options.autoFallbackOnStall ?? false;
|
|
1868
|
+
this.session = new AggregationService({
|
|
1869
|
+
// The coordinator never signs, so the state machine receives only the
|
|
1870
|
+
// public half of the operator's keypair (see ADR 038). The full keypair
|
|
1871
|
+
// remains the operator's transport/communication identity.
|
|
1872
|
+
did: options.did,
|
|
1873
|
+
publicKey: options.keys.publicKey,
|
|
1874
|
+
maxUpdateSizeBytes: options.maxUpdateSizeBytes
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
/** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
|
|
1878
|
+
#contextFor(msg) {
|
|
1879
|
+
const cohortId = msg.body?.cohortId;
|
|
1880
|
+
if (!cohortId) return void 0;
|
|
1881
|
+
return this.#contexts.get(cohortId);
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Drain any silent rejections the state machine recorded for a cohort during
|
|
1885
|
+
* the most recent receive() and surface them as `message-rejected` events.
|
|
1886
|
+
*/
|
|
1887
|
+
#drainRejections(ctx) {
|
|
1888
|
+
for (const r of this.session.drainRejections(ctx.cohortId)) {
|
|
1889
|
+
this.emit("message-rejected", { cohortId: ctx.cohortId, ...r });
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Advertise a new cohort and begin driving it to completion. Callable many
|
|
1894
|
+
* times on one runner; each cohort runs concurrently and independently.
|
|
1895
|
+
*
|
|
1896
|
+
* @param config Per-cohort conditions + network (see {@link CohortConfig}).
|
|
1897
|
+
* @returns The new cohort's id and a `completion` promise that resolves with
|
|
1898
|
+
* that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
|
|
1899
|
+
* @throws If the runner has been stopped, or the config is invalid
|
|
1900
|
+
* (fail-fast via `createCohort`).
|
|
1901
|
+
*/
|
|
1902
|
+
advertiseCohort(config) {
|
|
1903
|
+
if (this.#stopped) {
|
|
1904
|
+
throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
|
|
1905
|
+
}
|
|
1906
|
+
this.#registerHandlers();
|
|
1907
|
+
const cohortId = this.session.createCohort(config);
|
|
1908
|
+
let resolve;
|
|
1909
|
+
let reject;
|
|
1910
|
+
const completion = new Promise((res, rej) => {
|
|
1911
|
+
resolve = res;
|
|
1912
|
+
reject = rej;
|
|
1913
|
+
});
|
|
1914
|
+
const ctx = {
|
|
1915
|
+
cohortId,
|
|
1916
|
+
config,
|
|
1917
|
+
resolve,
|
|
1918
|
+
reject,
|
|
1919
|
+
completion,
|
|
1920
|
+
finalizing: false,
|
|
1921
|
+
settled: false
|
|
1922
|
+
};
|
|
1923
|
+
this.#contexts.set(cohortId, ctx);
|
|
1924
|
+
try {
|
|
1925
|
+
this.#startTimers(ctx);
|
|
1926
|
+
const advertMsgs = this.session.advertise(cohortId);
|
|
1927
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
1928
|
+
this.emit("cohort-advertised", { cohortId });
|
|
1929
|
+
if (this.#advertRepeatIntervalMs > 0) {
|
|
1930
|
+
this.#startAdvertRepeat(ctx, advertMsgs);
|
|
1931
|
+
} else {
|
|
1932
|
+
this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
|
|
1933
|
+
}
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
this.#failCohort(ctx, err);
|
|
1936
|
+
}
|
|
1937
|
+
return { cohortId, completion };
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Run a single cohort to completion using the `config` supplied in the
|
|
1941
|
+
* runner options. Thin convenience over {@link advertiseCohort} for the
|
|
1942
|
+
* single-cohort case (and the path {@link AggregationRunner.solo} rides).
|
|
1943
|
+
*
|
|
1944
|
+
* @returns {Promise<AggregationResult>} The final result with signature and signed tx.
|
|
1945
|
+
*/
|
|
1946
|
+
run() {
|
|
1947
|
+
if (!this.#defaultConfig) {
|
|
1948
|
+
return Promise.reject(new AggregationServiceError(
|
|
1949
|
+
"run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
|
|
1950
|
+
"MISSING_COHORT_CONFIG",
|
|
1951
|
+
{}
|
|
1952
|
+
));
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
return this.advertiseCohort(this.#defaultConfig).completion;
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
return Promise.reject(err);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Wait for every currently-outstanding cohort to settle and return the
|
|
1962
|
+
* successful results. Dynamic drain: cohorts advertised while this is pending
|
|
1963
|
+
* are included, and it resolves only once no cohorts remain. Failed cohorts
|
|
1964
|
+
* are surfaced via `error` / `cohort-failed` events and their rejected
|
|
1965
|
+
* `completion` promises; they are omitted from the returned array (this
|
|
1966
|
+
* method does not throw). Bound long-running cohorts with `cohortTtlMs` /
|
|
1967
|
+
* `phaseTimeoutMs` or this may never resolve.
|
|
1968
|
+
*
|
|
1969
|
+
* @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
|
|
1970
|
+
*/
|
|
1971
|
+
async runAll() {
|
|
1972
|
+
const collected = /* @__PURE__ */ new Map();
|
|
1973
|
+
const onComplete = (result) => {
|
|
1974
|
+
collected.set(result.cohortId, result);
|
|
1975
|
+
};
|
|
1976
|
+
this.on("signing-complete", onComplete);
|
|
1977
|
+
try {
|
|
1978
|
+
while (this.#contexts.size > 0) {
|
|
1979
|
+
await Promise.allSettled([...this.#contexts.values()].map((c) => c.completion));
|
|
1980
|
+
}
|
|
1981
|
+
} finally {
|
|
1982
|
+
this.off("signing-complete", onComplete);
|
|
1983
|
+
}
|
|
1984
|
+
return [...collected.values()];
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Begin publishing a cohort's advert immediately and on a repeating interval
|
|
1988
|
+
* until the cohort's advert loop is stopped. Each advert is broadcast (no
|
|
1989
|
+
* recipient) via the transport's `publishRepeating` primitive.
|
|
1990
|
+
*/
|
|
1991
|
+
#startAdvertRepeat(ctx, advertMsgs) {
|
|
1992
|
+
const stops = [];
|
|
1993
|
+
for (const msg of advertMsgs) {
|
|
1994
|
+
stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
|
|
1995
|
+
}
|
|
1996
|
+
ctx.stopAdvertRepeat = () => {
|
|
1997
|
+
for (const stop of stops) {
|
|
1998
|
+
try {
|
|
1999
|
+
stop();
|
|
2000
|
+
} catch {
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
/** Stop a cohort's advert republish loop. Idempotent. */
|
|
2006
|
+
#stopAdvertRepeating(ctx) {
|
|
2007
|
+
if (!ctx.stopAdvertRepeat) return;
|
|
2008
|
+
const stop = ctx.stopAdvertRepeat;
|
|
2009
|
+
ctx.stopAdvertRepeat = void 0;
|
|
2010
|
+
stop();
|
|
2011
|
+
}
|
|
2012
|
+
/** Schedule a cohort's TTL + phase timeout when it is advertised. */
|
|
2013
|
+
#startTimers(ctx) {
|
|
2014
|
+
if (this.#cohortTtlMs !== void 0) {
|
|
2015
|
+
ctx.cohortTtlTimer = setTimeout(() => {
|
|
2016
|
+
const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
|
|
2017
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
2018
|
+
this.#failCohort(ctx, new Error(reason));
|
|
2019
|
+
}, this.#cohortTtlMs);
|
|
2020
|
+
}
|
|
2021
|
+
this.#resetPhaseTimer(ctx);
|
|
2022
|
+
}
|
|
2023
|
+
/** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
|
|
2024
|
+
#resetPhaseTimer(ctx) {
|
|
2025
|
+
if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
2026
|
+
ctx.phaseTimer = void 0;
|
|
2027
|
+
if (this.#phaseTimeoutMs === void 0) return;
|
|
2028
|
+
ctx.phaseTimer = setTimeout(() => {
|
|
2029
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
2030
|
+
const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
|
|
2031
|
+
if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
|
|
2032
|
+
this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
2036
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
2037
|
+
this.#failCohort(ctx, new Error(reason));
|
|
2038
|
+
}, this.#phaseTimeoutMs);
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
|
|
2042
|
+
* fallback (script-path) signatures instead (ADR 042). Idempotent and safe
|
|
2043
|
+
* against the optimistic completion: it commits the cohort to the fallback
|
|
2044
|
+
* path synchronously (the `committedPath` latch) before sending anything, so a
|
|
2045
|
+
* late optimistic signature can no longer complete-and-broadcast a competing
|
|
2046
|
+
* spend of the same UTXO. No-op if the cohort is unknown, already settled, or
|
|
2047
|
+
* already committed to a path.
|
|
2048
|
+
*
|
|
2049
|
+
* Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
|
|
2050
|
+
* set; otherwise call it from an operator decision (a UI "fall back now"
|
|
2051
|
+
* action). Throws only if the underlying state machine rejects the transition
|
|
2052
|
+
* (e.g. signing has not started).
|
|
2053
|
+
*/
|
|
2054
|
+
async triggerFallback(cohortId) {
|
|
2055
|
+
const ctx = this.#contexts.get(cohortId);
|
|
2056
|
+
if (!ctx || ctx.settled || ctx.committedPath) return;
|
|
2057
|
+
const messages = this.session.startFallbackSigning(cohortId);
|
|
2058
|
+
ctx.committedPath = "fallback";
|
|
2059
|
+
this.#stopAdvertRepeating(ctx);
|
|
2060
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2061
|
+
const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
|
|
2062
|
+
this.emit("fallback-started", { cohortId, sessionId });
|
|
2063
|
+
await this.#sendAll(messages);
|
|
2064
|
+
}
|
|
2065
|
+
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
2066
|
+
#onPhaseMaybeChanged(ctx) {
|
|
2067
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
2068
|
+
if (phase !== ctx.lastObservedPhase) {
|
|
2069
|
+
ctx.lastObservedPhase = phase;
|
|
2070
|
+
this.#resetPhaseTimer(ctx);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
/** Clear a cohort's timers. Called on completion, stop, and failure. */
|
|
2074
|
+
#clearTimers(ctx) {
|
|
2075
|
+
if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
|
|
2076
|
+
if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
2077
|
+
ctx.cohortTtlTimer = void 0;
|
|
2078
|
+
ctx.phaseTimer = void 0;
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
|
|
2082
|
+
* its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
|
|
2083
|
+
* and does NOT detach the shared transport handlers. Leaves the cohort in the
|
|
2084
|
+
* state machine; whether that cohort's `session` state is also removed is the
|
|
2085
|
+
* caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
|
|
2086
|
+
*/
|
|
2087
|
+
#disposeCohort(ctx) {
|
|
2088
|
+
this.#stopAdvertRepeating(ctx);
|
|
2089
|
+
this.#clearTimers(ctx);
|
|
2090
|
+
this.#contexts.delete(ctx.cohortId);
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Settle one cohort successfully. Reclaims the runner context but leaves the
|
|
2094
|
+
* completed cohort in `session` so callers can read its beaconAddress / cohort
|
|
2095
|
+
* via `session.getCohort(result.cohortId)`; reclaim it with
|
|
2096
|
+
* `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
|
|
2097
|
+
*/
|
|
2098
|
+
#completeCohort(ctx, result) {
|
|
2099
|
+
if (ctx.settled) return;
|
|
2100
|
+
ctx.settled = true;
|
|
2101
|
+
this.#disposeCohort(ctx);
|
|
2102
|
+
this.emit("signing-complete", result);
|
|
2103
|
+
ctx.resolve(result);
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Fail one cohort. Reclaims its runner context, drops its now-dead state from
|
|
2107
|
+
* the state machine, and rejects only its completion; siblings keep running
|
|
2108
|
+
* and the shared transport handlers stay registered. Idempotent via
|
|
2109
|
+
* `ctx.settled`.
|
|
2110
|
+
*/
|
|
2111
|
+
#failCohort(ctx, err) {
|
|
2112
|
+
if (ctx.settled) return;
|
|
2113
|
+
ctx.settled = true;
|
|
2114
|
+
this.#disposeCohort(ctx);
|
|
2115
|
+
this.session.removeCohort(ctx.cohortId);
|
|
2116
|
+
this.emit("error", err);
|
|
2117
|
+
ctx.reject(err);
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Stop a single cohort early without affecting the rest of the runner. Drops
|
|
2121
|
+
* the cohort's state machine state; its `completion` promise rejects with a
|
|
2122
|
+
* stopped error.
|
|
2123
|
+
*/
|
|
2124
|
+
stopCohort(cohortId) {
|
|
2125
|
+
const ctx = this.#contexts.get(cohortId);
|
|
2126
|
+
if (!ctx || ctx.settled) return;
|
|
2127
|
+
ctx.settled = true;
|
|
2128
|
+
this.#disposeCohort(ctx);
|
|
2129
|
+
this.session.removeCohort(cohortId);
|
|
2130
|
+
ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Stop the whole runner. Fails every outstanding cohort, then detaches the
|
|
2134
|
+
* shared transport handlers so a restart or a new runner doesn't inherit
|
|
2135
|
+
* stale dispatch. Safe to call repeatedly.
|
|
2136
|
+
*/
|
|
2137
|
+
stop() {
|
|
2138
|
+
this.#stopped = true;
|
|
2139
|
+
for (const ctx of [...this.#contexts.values()]) {
|
|
2140
|
+
if (ctx.settled) continue;
|
|
2141
|
+
ctx.settled = true;
|
|
2142
|
+
this.#disposeCohort(ctx);
|
|
2143
|
+
this.session.removeCohort(ctx.cohortId);
|
|
2144
|
+
ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
|
|
2145
|
+
}
|
|
2146
|
+
this.#contexts.clear();
|
|
2147
|
+
this.#unregisterHandlers();
|
|
2148
|
+
}
|
|
2149
|
+
/** Message types this runner listens for on the transport. */
|
|
2150
|
+
static #HANDLED_MESSAGE_TYPES = [
|
|
2151
|
+
COHORT_OPT_IN,
|
|
2152
|
+
SUBMIT_UPDATE,
|
|
2153
|
+
SUBMIT_NONINCLUDED,
|
|
2154
|
+
VALIDATION_ACK,
|
|
2155
|
+
NONCE_CONTRIBUTION,
|
|
2156
|
+
SIGNATURE_AUTHORIZATION,
|
|
2157
|
+
FALLBACK_SIGNATURE
|
|
2158
|
+
];
|
|
2159
|
+
/**
|
|
2160
|
+
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
2161
|
+
* are DID-scoped and cohort-agnostic - one registration serves every cohort
|
|
2162
|
+
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
2163
|
+
* handler via the inbound message's cohortId.
|
|
2164
|
+
*/
|
|
2165
|
+
#registerHandlers() {
|
|
2166
|
+
if (this.#handlersRegistered) return;
|
|
2167
|
+
this.#handlersRegistered = true;
|
|
2168
|
+
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
|
|
2169
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
|
|
2170
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
|
|
2171
|
+
this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
|
|
2172
|
+
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
2173
|
+
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
2174
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
|
|
2175
|
+
}
|
|
2176
|
+
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
2177
|
+
#unregisterHandlers() {
|
|
2178
|
+
if (!this.#handlersRegistered) return;
|
|
2179
|
+
this.#handlersRegistered = false;
|
|
2180
|
+
for (const type of _AggregationServiceRunner.#HANDLED_MESSAGE_TYPES) {
|
|
2181
|
+
this.#transport.unregisterMessageHandler(this.#did, type);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Internal: message handlers for each protocol step. Each handler:
|
|
2186
|
+
* 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
|
|
2187
|
+
* 2) feeds the message into the state machine via session.receive()
|
|
2188
|
+
* 3) emits a high-level event (carrying cohortId) for external observers
|
|
2189
|
+
* 4) checks if the new state triggers any automatic next steps, and if so:
|
|
2190
|
+
* a) calls the appropriate decision callback(s)
|
|
2191
|
+
* b) sends any resulting messages from the state machine
|
|
2192
|
+
* Errors fail only the owning cohort. A stopped runner ignores messages.
|
|
2193
|
+
* @param {BaseMessage} msg - The incoming message to handle.
|
|
2194
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2195
|
+
*/
|
|
2196
|
+
async #handleOptIn(msg) {
|
|
2197
|
+
if (this.#stopped) return;
|
|
2198
|
+
const ctx = this.#contextFor(msg);
|
|
2199
|
+
if (!ctx) return;
|
|
2200
|
+
try {
|
|
2201
|
+
this.session.receive(msg);
|
|
2202
|
+
this.#drainRejections(ctx);
|
|
2203
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2204
|
+
const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
|
|
2205
|
+
if (!optIn) return;
|
|
2206
|
+
this.emit("opt-in-received", optIn);
|
|
2207
|
+
if (optIn.communicationPk) {
|
|
2208
|
+
this.#transport.registerPeer(msg.from, optIn.communicationPk);
|
|
2209
|
+
}
|
|
2210
|
+
const decision = await this.#onOptInReceived(optIn);
|
|
2211
|
+
if (!decision.accepted) return;
|
|
2212
|
+
const maxParticipants = ctx.config.maxParticipants;
|
|
2213
|
+
const cohortNow = this.session.getCohort(ctx.cohortId);
|
|
2214
|
+
if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
|
|
2218
|
+
this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
2219
|
+
const cohort = this.session.getCohort(ctx.cohortId);
|
|
2220
|
+
if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
|
|
2221
|
+
ctx.finalizing = true;
|
|
2222
|
+
const finalizeDecision = await this.#onReadyToFinalize({
|
|
2223
|
+
acceptedCount: cohort.participants.length,
|
|
2224
|
+
minRequired: ctx.config.minParticipants
|
|
2225
|
+
});
|
|
2226
|
+
if (!finalizeDecision.finalize) {
|
|
2227
|
+
ctx.finalizing = false;
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
|
|
2231
|
+
this.#stopAdvertRepeating(ctx);
|
|
2232
|
+
this.emit("keygen-complete", {
|
|
2233
|
+
cohortId: ctx.cohortId,
|
|
2234
|
+
beaconAddress: cohort.beaconAddress
|
|
2235
|
+
});
|
|
2236
|
+
await this.#sendAll(readyMsgs);
|
|
2237
|
+
}
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
this.#failCohort(ctx, err);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Handler for receiving participant updates. When all updates are received, automatically builds
|
|
2244
|
+
* and distributes the data for validation.
|
|
2245
|
+
* @param {BaseMessage} msg - The incoming message to handle.
|
|
2246
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2247
|
+
*/
|
|
2248
|
+
async #handleSubmitUpdate(msg) {
|
|
2249
|
+
if (this.#stopped) return;
|
|
2250
|
+
const ctx = this.#contextFor(msg);
|
|
2251
|
+
if (!ctx) return;
|
|
2252
|
+
try {
|
|
2253
|
+
this.session.receive(msg);
|
|
2254
|
+
this.#drainRejections(ctx);
|
|
2255
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2256
|
+
this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
2257
|
+
if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
|
|
2258
|
+
const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
|
|
2259
|
+
this.emit("data-distributed", { cohortId: ctx.cohortId });
|
|
2260
|
+
await this.#sendAll(distributeMsgs);
|
|
2261
|
+
}
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
this.#failCohort(ctx, err);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Handler for receiving validation acknowledgments. When all validations are received,
|
|
2268
|
+
* automatically requests tx data and starts signing.
|
|
2269
|
+
* @param {BaseMessage} msg - The incoming message to handle.
|
|
2270
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2271
|
+
*/
|
|
2272
|
+
async #handleValidationAck(msg) {
|
|
2273
|
+
if (this.#stopped) return;
|
|
2274
|
+
const ctx = this.#contextFor(msg);
|
|
2275
|
+
if (!ctx) return;
|
|
2276
|
+
try {
|
|
2277
|
+
this.session.receive(msg);
|
|
2278
|
+
this.#drainRejections(ctx);
|
|
2279
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2280
|
+
const approved = !!msg.body?.approved;
|
|
2281
|
+
this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
|
|
2282
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
2283
|
+
if (phase === "Failed" /* Failed */) {
|
|
2284
|
+
const reason = `Validation rejected by participant ${msg.from}`;
|
|
2285
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
2286
|
+
this.#failCohort(ctx, new Error(reason));
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
if (phase === "Validated" /* Validated */) {
|
|
2290
|
+
const cohort = this.session.getCohort(ctx.cohortId);
|
|
2291
|
+
const txData = await this.#onProvideTxData({
|
|
2292
|
+
cohortId: ctx.cohortId,
|
|
2293
|
+
beaconAddress: cohort.beaconAddress,
|
|
2294
|
+
signalBytes: cohort.signalBytes,
|
|
2295
|
+
feeEstimator: this.#feeEstimator
|
|
2296
|
+
});
|
|
2297
|
+
const authMsgs = this.session.startSigning(ctx.cohortId, txData);
|
|
2298
|
+
const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
|
|
2299
|
+
this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
|
|
2300
|
+
await this.#sendAll(authMsgs);
|
|
2301
|
+
}
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
this.#failCohort(ctx, err);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
|
|
2308
|
+
* nonce back to the cohort.
|
|
2309
|
+
* @param {BaseMessage} msg - The incoming message to handle.
|
|
2310
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2311
|
+
*/
|
|
2312
|
+
async #handleNonceContribution(msg) {
|
|
2313
|
+
if (this.#stopped) return;
|
|
2314
|
+
const ctx = this.#contextFor(msg);
|
|
2315
|
+
if (!ctx) return;
|
|
2316
|
+
try {
|
|
2317
|
+
this.session.receive(msg);
|
|
2318
|
+
this.#drainRejections(ctx);
|
|
2319
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2320
|
+
this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
2321
|
+
if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
|
|
2322
|
+
await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
|
|
2323
|
+
}
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
this.#failCohort(ctx, err);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Handler for receiving signature authorizations. When all partial signatures are received, the
|
|
2330
|
+
* session automatically completes; the final result is emitted and the cohort's completion
|
|
2331
|
+
* promise resolves.
|
|
2332
|
+
* @param {BaseMessage} msg - The incoming message to handle.
|
|
2333
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2334
|
+
*/
|
|
2335
|
+
async #handleSignatureAuthorization(msg) {
|
|
2336
|
+
if (this.#stopped) return;
|
|
2337
|
+
const ctx = this.#contextFor(msg);
|
|
2338
|
+
if (!ctx) return;
|
|
2339
|
+
try {
|
|
2340
|
+
this.session.receive(msg);
|
|
2341
|
+
this.#drainRejections(ctx);
|
|
2342
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2343
|
+
if (ctx.committedPath === "fallback") return;
|
|
2344
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
2345
|
+
if (result) {
|
|
2346
|
+
ctx.committedPath = "optimistic";
|
|
2347
|
+
this.#completeCohort(ctx, result);
|
|
2348
|
+
}
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
this.#failCohort(ctx, err);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Handler for receiving fallback (k-of-n script-path) signatures. The state
|
|
2355
|
+
* machine assembles and finalizes the fallback spend once k valid signatures
|
|
2356
|
+
* are in; the result is then emitted and the cohort's completion resolves. The
|
|
2357
|
+
* cohort is already committed to the fallback path (via {@link triggerFallback}).
|
|
2358
|
+
* @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
|
|
2359
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
2360
|
+
*/
|
|
2361
|
+
async #handleFallbackSignature(msg) {
|
|
2362
|
+
if (this.#stopped) return;
|
|
2363
|
+
const ctx = this.#contextFor(msg);
|
|
2364
|
+
if (!ctx) return;
|
|
2365
|
+
try {
|
|
2366
|
+
this.session.receive(msg);
|
|
2367
|
+
this.#drainRejections(ctx);
|
|
2368
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
2369
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
2370
|
+
if (result) {
|
|
2371
|
+
this.#completeCohort(ctx, result);
|
|
2372
|
+
}
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
this.#failCohort(ctx, err);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Internal: helper to send all messages sequentially. Catches and propagates errors.
|
|
2379
|
+
* @param {BaseMessage[]} msgs - The messages to send.
|
|
2380
|
+
* @returns {Promise<void>} Resolves when all messages have been sent.
|
|
2381
|
+
*/
|
|
2382
|
+
async #sendAll(msgs) {
|
|
2383
|
+
for (const m of msgs) {
|
|
2384
|
+
await this.#transport.sendMessage(m, this.#did, m.to);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/service/http-server.ts
|
|
2390
|
+
var import_keypair2 = require("@did-btcr2/keypair");
|
|
2391
|
+
|
|
2392
|
+
// src/core/logger.ts
|
|
2393
|
+
var CONSOLE_LOGGER = {
|
|
2394
|
+
debug: (msg, ...args) => console.debug(msg, ...args),
|
|
2395
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
2396
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
2397
|
+
error: (msg, ...args) => console.error(msg, ...args)
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
// src/core/transport/http/envelope.ts
|
|
2401
|
+
var import_common6 = require("@did-btcr2/common");
|
|
2402
|
+
var import_utils4 = require("@noble/hashes/utils");
|
|
2403
|
+
|
|
2404
|
+
// src/core/transport/error.ts
|
|
2405
|
+
var import_common5 = require("@did-btcr2/common");
|
|
2406
|
+
var TransportAdapterError = class extends import_common5.MethodError {
|
|
2407
|
+
constructor(message, type = "TransportAdapterError", data) {
|
|
2408
|
+
super(message, type, data);
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
// src/core/transport/http/errors.ts
|
|
2413
|
+
var HttpTransportError = class extends TransportAdapterError {
|
|
2414
|
+
constructor(message, type = "HttpTransportError", data) {
|
|
2415
|
+
super(message, type, { adapter: "http", ...data ?? {} });
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
// src/core/transport/http/protocol.ts
|
|
2420
|
+
var HTTP_ENVELOPE_VERSION = 1;
|
|
2421
|
+
var HTTP_ROUTE = {
|
|
2422
|
+
ADVERTS: "/v1/adverts",
|
|
2423
|
+
MESSAGES: "/v1/messages",
|
|
2424
|
+
ACTOR_INBOX: "/v1/actors/{did}/inbox",
|
|
2425
|
+
WELL_KNOWN: "/v1/.well-known/aggregation"
|
|
2426
|
+
};
|
|
2427
|
+
var SSE_EVENT = {
|
|
2428
|
+
ADVERT: "advert",
|
|
2429
|
+
MESSAGE: "message",
|
|
2430
|
+
HEARTBEAT: "heartbeat"
|
|
2431
|
+
};
|
|
2432
|
+
var DEFAULT_CLOCK_SKEW_SEC = 60;
|
|
2433
|
+
var DEFAULT_NONCE_LEN_BYTES = 16;
|
|
2434
|
+
|
|
2435
|
+
// src/core/transport/http/envelope.ts
|
|
2436
|
+
function signEnvelope(message, sender, opts = {}) {
|
|
2437
|
+
const timestamp = opts.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
2438
|
+
const nonce = opts.nonce ?? (0, import_utils4.bytesToHex)((0, import_utils4.randomBytes)(DEFAULT_NONCE_LEN_BYTES));
|
|
2439
|
+
const messageJson = normalizeForWire(normalizeMessage(message));
|
|
2440
|
+
const unsigned = {
|
|
2441
|
+
v: HTTP_ENVELOPE_VERSION,
|
|
2442
|
+
from: sender.did,
|
|
2443
|
+
...opts.to !== void 0 ? { to: opts.to } : {},
|
|
2444
|
+
timestamp,
|
|
2445
|
+
nonce,
|
|
2446
|
+
message: messageJson
|
|
2447
|
+
};
|
|
2448
|
+
const digest = (0, import_common6.canonicalHashBytes)(unsigned);
|
|
2449
|
+
const sig = sender.keys.secretKey.sign(digest, { scheme: "schnorr" });
|
|
2450
|
+
return { ...unsigned, sig: (0, import_utils4.bytesToHex)(sig) };
|
|
2451
|
+
}
|
|
2452
|
+
function verifyEnvelope(envelope, senderPk, opts = {}) {
|
|
2453
|
+
if (envelope.v !== HTTP_ENVELOPE_VERSION) {
|
|
2454
|
+
throw new HttpTransportError(
|
|
2455
|
+
`Unsupported envelope version: ${envelope.v}`,
|
|
2456
|
+
"ENVELOPE_VERSION_MISMATCH",
|
|
2457
|
+
{ version: envelope.v, expected: HTTP_ENVELOPE_VERSION }
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
if (opts.expectedFrom !== void 0 && envelope.from !== opts.expectedFrom) {
|
|
2461
|
+
throw new HttpTransportError(
|
|
2462
|
+
`Envelope from mismatch: expected ${opts.expectedFrom}, got ${envelope.from}`,
|
|
2463
|
+
"ENVELOPE_FROM_MISMATCH",
|
|
2464
|
+
{ expected: opts.expectedFrom, got: envelope.from }
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
if ("expectedTo" in opts && envelope.to !== opts.expectedTo) {
|
|
2468
|
+
throw new HttpTransportError(
|
|
2469
|
+
`Envelope to mismatch: expected ${opts.expectedTo ?? "<broadcast>"}, got ${envelope.to ?? "<broadcast>"}`,
|
|
2470
|
+
"ENVELOPE_TO_MISMATCH",
|
|
2471
|
+
{ expected: opts.expectedTo, got: envelope.to }
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
2475
|
+
const nowMs = opts.now ? opts.now() : Date.now();
|
|
2476
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
2477
|
+
const diff = Math.abs(nowSec - envelope.timestamp);
|
|
2478
|
+
if (diff > skewSec) {
|
|
2479
|
+
throw new HttpTransportError(
|
|
2480
|
+
`Envelope timestamp out of skew: ${diff}s > ${skewSec}s`,
|
|
2481
|
+
"ENVELOPE_TIMESTAMP_SKEW",
|
|
2482
|
+
{ diff, skewSec, timestamp: envelope.timestamp, now: nowSec }
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
let sigBytes;
|
|
2486
|
+
try {
|
|
2487
|
+
sigBytes = (0, import_utils4.hexToBytes)(envelope.sig);
|
|
2488
|
+
} catch {
|
|
2489
|
+
throw new HttpTransportError(
|
|
2490
|
+
"Envelope signature is not valid hex",
|
|
2491
|
+
"ENVELOPE_SIG_HEX"
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
if (sigBytes.length !== 64) {
|
|
2495
|
+
throw new HttpTransportError(
|
|
2496
|
+
`Invalid signature length: ${sigBytes.length} (expected 64)`,
|
|
2497
|
+
"ENVELOPE_SIG_LENGTH",
|
|
2498
|
+
{ length: sigBytes.length }
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const { sig: _sig, ...unsigned } = envelope;
|
|
2502
|
+
const digest = (0, import_common6.canonicalHashBytes)(unsigned);
|
|
2503
|
+
const ok = senderPk.verify(sigBytes, digest, { scheme: "schnorr" });
|
|
2504
|
+
if (!ok) {
|
|
2505
|
+
throw new HttpTransportError(
|
|
2506
|
+
"Envelope signature verification failed",
|
|
2507
|
+
"ENVELOPE_SIG_INVALID"
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
function normalizeMessage(message) {
|
|
2512
|
+
const maybeToJSON = message.toJSON;
|
|
2513
|
+
if (typeof maybeToJSON === "function") {
|
|
2514
|
+
return maybeToJSON.call(message);
|
|
2515
|
+
}
|
|
2516
|
+
return message;
|
|
2517
|
+
}
|
|
2518
|
+
function normalizeForWire(value) {
|
|
2519
|
+
if (value instanceof Uint8Array) {
|
|
2520
|
+
return { __bytes: (0, import_utils4.bytesToHex)(value) };
|
|
2521
|
+
}
|
|
2522
|
+
if (Array.isArray(value)) {
|
|
2523
|
+
return value.map((v) => normalizeForWire(v));
|
|
2524
|
+
}
|
|
2525
|
+
if (value && typeof value === "object") {
|
|
2526
|
+
const out = {};
|
|
2527
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2528
|
+
out[k] = normalizeForWire(v);
|
|
2529
|
+
}
|
|
2530
|
+
return out;
|
|
2531
|
+
}
|
|
2532
|
+
return value;
|
|
2533
|
+
}
|
|
2534
|
+
function reviveFromWire(value) {
|
|
2535
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2536
|
+
const rec = value;
|
|
2537
|
+
const keys = Object.keys(rec);
|
|
2538
|
+
if (keys.length === 1 && keys[0] === "__bytes" && typeof rec.__bytes === "string") {
|
|
2539
|
+
return (0, import_utils4.hexToBytes)(rec.__bytes);
|
|
2540
|
+
}
|
|
2541
|
+
const out = {};
|
|
2542
|
+
for (const [k, v] of Object.entries(rec)) out[k] = reviveFromWire(v);
|
|
2543
|
+
return out;
|
|
2544
|
+
}
|
|
2545
|
+
if (Array.isArray(value)) {
|
|
2546
|
+
return value.map((v) => reviveFromWire(v));
|
|
2547
|
+
}
|
|
2548
|
+
return value;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// src/service/inbox-buffer.ts
|
|
2552
|
+
var InboxBuffer = class {
|
|
2553
|
+
#capacity;
|
|
2554
|
+
#entries = [];
|
|
2555
|
+
#nextId = 1;
|
|
2556
|
+
constructor(capacity = 100) {
|
|
2557
|
+
if (capacity < 1) throw new Error(`InboxBuffer capacity must be >= 1; got ${capacity}`);
|
|
2558
|
+
this.#capacity = capacity;
|
|
2559
|
+
}
|
|
2560
|
+
/** Append an event. Returns the stored record (including its assigned id). */
|
|
2561
|
+
append(event, data) {
|
|
2562
|
+
const stored = { id: String(this.#nextId++), event, data };
|
|
2563
|
+
this.#entries.push(stored);
|
|
2564
|
+
if (this.#entries.length > this.#capacity) this.#entries.shift();
|
|
2565
|
+
return stored;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Return stored events with id strictly greater than `lastEventId`. If
|
|
2569
|
+
* `lastEventId` is unset or unparseable, returns everything currently
|
|
2570
|
+
* retained.
|
|
2571
|
+
*/
|
|
2572
|
+
since(lastEventId) {
|
|
2573
|
+
if (!lastEventId) return this.#entries.slice();
|
|
2574
|
+
const boundary = Number(lastEventId);
|
|
2575
|
+
if (!Number.isFinite(boundary)) return this.#entries.slice();
|
|
2576
|
+
return this.#entries.filter((e) => Number(e.id) > boundary);
|
|
2577
|
+
}
|
|
2578
|
+
/** Currently retained event count. */
|
|
2579
|
+
size() {
|
|
2580
|
+
return this.#entries.length;
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
// src/service/nonce-cache.ts
|
|
2585
|
+
var NonceCache = class {
|
|
2586
|
+
#maxEntries;
|
|
2587
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2588
|
+
constructor(config = {}) {
|
|
2589
|
+
this.#maxEntries = config.maxEntries ?? 1e4;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Record a nonce. Returns `true` if it was novel (caller should accept the
|
|
2593
|
+
* request) or `false` if it was a replay (caller should reject).
|
|
2594
|
+
*/
|
|
2595
|
+
store(did, nonce, timestampSec) {
|
|
2596
|
+
const key = `${did}:${nonce}`;
|
|
2597
|
+
if (this.#entries.has(key)) return false;
|
|
2598
|
+
this.#entries.set(key, timestampSec);
|
|
2599
|
+
if (this.#entries.size > this.#maxEntries) {
|
|
2600
|
+
const oldest = this.#entries.keys().next();
|
|
2601
|
+
if (!oldest.done) this.#entries.delete(oldest.value);
|
|
2602
|
+
}
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
/** Current cache size. Exposed for observability and tests. */
|
|
2606
|
+
size() {
|
|
2607
|
+
return this.#entries.size;
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/service/rate-limiter.ts
|
|
2612
|
+
var InMemoryRateLimitStore = class {
|
|
2613
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
2614
|
+
get(key) {
|
|
2615
|
+
return this.#buckets.get(key);
|
|
2616
|
+
}
|
|
2617
|
+
set(key, state) {
|
|
2618
|
+
this.#buckets.set(key, state);
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
var RateLimiter = class {
|
|
2622
|
+
#rps;
|
|
2623
|
+
#burst;
|
|
2624
|
+
#store;
|
|
2625
|
+
constructor(config = {}) {
|
|
2626
|
+
this.#rps = config.rps ?? 10;
|
|
2627
|
+
this.#burst = config.burst ?? 30;
|
|
2628
|
+
this.#store = config.store ?? new InMemoryRateLimitStore();
|
|
2629
|
+
}
|
|
2630
|
+
/** Consume one token for `key`. Returns `true` if accepted, `false` if throttled. */
|
|
2631
|
+
consume(key, nowMs) {
|
|
2632
|
+
const existing = this.#store.get(key);
|
|
2633
|
+
const state = existing ?? { tokens: this.#burst, lastRefillMs: nowMs };
|
|
2634
|
+
if (existing) {
|
|
2635
|
+
const elapsedSec = Math.max(0, (nowMs - existing.lastRefillMs) / 1e3);
|
|
2636
|
+
state.tokens = Math.min(this.#burst, existing.tokens + elapsedSec * this.#rps);
|
|
2637
|
+
state.lastRefillMs = nowMs;
|
|
2638
|
+
}
|
|
2639
|
+
if (state.tokens < 1) {
|
|
2640
|
+
this.#store.set(key, state);
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
state.tokens -= 1;
|
|
2644
|
+
this.#store.set(key, state);
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
|
|
2649
|
+
// src/core/transport/http/request-auth.ts
|
|
2650
|
+
var import_common7 = require("@did-btcr2/common");
|
|
2651
|
+
var import_utils5 = require("@noble/hashes/utils");
|
|
2652
|
+
var REQUEST_AUTH_SCHEME = "BTCR2-Sig";
|
|
2653
|
+
function parseRequestAuth(headerValue) {
|
|
2654
|
+
const prefix = `${REQUEST_AUTH_SCHEME} `;
|
|
2655
|
+
if (!headerValue.startsWith(prefix)) {
|
|
2656
|
+
throw new HttpTransportError(
|
|
2657
|
+
`Unexpected auth scheme (want ${REQUEST_AUTH_SCHEME})`,
|
|
2658
|
+
"REQUEST_AUTH_SCHEME"
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
const params = {};
|
|
2662
|
+
for (const piece of headerValue.slice(prefix.length).split(",")) {
|
|
2663
|
+
const eq = piece.indexOf("=");
|
|
2664
|
+
if (eq === -1) continue;
|
|
2665
|
+
const key = piece.slice(0, eq).trim();
|
|
2666
|
+
const val = piece.slice(eq + 1).trim();
|
|
2667
|
+
if (key.length > 0) params[key] = val;
|
|
2668
|
+
}
|
|
2669
|
+
const v = Number(params.v);
|
|
2670
|
+
const ts = Number(params.ts);
|
|
2671
|
+
if (!Number.isInteger(v) || !Number.isInteger(ts) || !params.did || !params.nonce || !params.sig) {
|
|
2672
|
+
throw new HttpTransportError(
|
|
2673
|
+
"Malformed auth header (missing or invalid field)",
|
|
2674
|
+
"REQUEST_AUTH_MALFORMED",
|
|
2675
|
+
{ received: Object.keys(params) }
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
return { v, did: params.did, ts, nonce: params.nonce, sig: params.sig };
|
|
2679
|
+
}
|
|
2680
|
+
function verifyRequestAuth(headerValue, expectedPath, senderPk, opts = {}) {
|
|
2681
|
+
const parsed = parseRequestAuth(headerValue);
|
|
2682
|
+
if (parsed.v !== HTTP_ENVELOPE_VERSION) {
|
|
2683
|
+
throw new HttpTransportError(
|
|
2684
|
+
`Unsupported auth version: ${parsed.v}`,
|
|
2685
|
+
"REQUEST_AUTH_VERSION_MISMATCH",
|
|
2686
|
+
{ version: parsed.v, expected: HTTP_ENVELOPE_VERSION }
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
2690
|
+
const nowMs = opts.now ? opts.now() : Date.now();
|
|
2691
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
2692
|
+
const diff = Math.abs(nowSec - parsed.ts);
|
|
2693
|
+
if (diff > skewSec) {
|
|
2694
|
+
throw new HttpTransportError(
|
|
2695
|
+
`Auth timestamp out of skew: ${diff}s > ${skewSec}s`,
|
|
2696
|
+
"REQUEST_AUTH_TIMESTAMP_SKEW",
|
|
2697
|
+
{ diff, skewSec }
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
let sigBytes;
|
|
2701
|
+
try {
|
|
2702
|
+
sigBytes = (0, import_utils5.hexToBytes)(parsed.sig);
|
|
2703
|
+
} catch {
|
|
2704
|
+
throw new HttpTransportError("Auth signature is not valid hex", "REQUEST_AUTH_SIG_HEX");
|
|
2705
|
+
}
|
|
2706
|
+
if (sigBytes.length !== 64) {
|
|
2707
|
+
throw new HttpTransportError(
|
|
2708
|
+
`Invalid auth signature length: ${sigBytes.length}`,
|
|
2709
|
+
"REQUEST_AUTH_SIG_LENGTH",
|
|
2710
|
+
{ length: sigBytes.length }
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const digest = (0, import_common7.canonicalHashBytes)({
|
|
2714
|
+
v: parsed.v,
|
|
2715
|
+
did: parsed.did,
|
|
2716
|
+
ts: parsed.ts,
|
|
2717
|
+
nonce: parsed.nonce,
|
|
2718
|
+
path: expectedPath
|
|
2719
|
+
});
|
|
2720
|
+
const ok = senderPk.verify(sigBytes, digest, { scheme: "schnorr" });
|
|
2721
|
+
if (!ok) {
|
|
2722
|
+
throw new HttpTransportError("Auth signature verification failed", "REQUEST_AUTH_SIG_INVALID");
|
|
2723
|
+
}
|
|
2724
|
+
return parsed;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// src/service/http-server.ts
|
|
2728
|
+
var INBOX_PATH_PREFIX = "/v1/actors/";
|
|
2729
|
+
var INBOX_PATH_SUFFIX = "/inbox";
|
|
2730
|
+
var DEFAULT_ADVERT_TTL_MS = 5 * 60 * 1e3;
|
|
2731
|
+
var DEFAULT_HEARTBEAT_MS = 2e4;
|
|
2732
|
+
var HttpServerTransport = class {
|
|
2733
|
+
name = "http";
|
|
2734
|
+
#logger;
|
|
2735
|
+
#cors;
|
|
2736
|
+
#clockSkewSec;
|
|
2737
|
+
#inboxBufferSize;
|
|
2738
|
+
#advertTtlMs;
|
|
2739
|
+
#heartbeatMs;
|
|
2740
|
+
#rateLimiter;
|
|
2741
|
+
#nonceCache;
|
|
2742
|
+
#now;
|
|
2743
|
+
#resolveSenderPkFn;
|
|
2744
|
+
#actors = /* @__PURE__ */ new Map();
|
|
2745
|
+
#peers = /* @__PURE__ */ new Map();
|
|
2746
|
+
#inboxes = /* @__PURE__ */ new Map();
|
|
2747
|
+
#broadcastSubscribers = /* @__PURE__ */ new Set();
|
|
2748
|
+
#currentAdvert;
|
|
2749
|
+
#advertSeq = 0;
|
|
2750
|
+
constructor(config = {}) {
|
|
2751
|
+
this.#logger = config.logger ?? CONSOLE_LOGGER;
|
|
2752
|
+
this.#cors = config.cors ?? { mode: "permissive" };
|
|
2753
|
+
this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
|
|
2754
|
+
this.#inboxBufferSize = config.inboxBufferSize ?? 100;
|
|
2755
|
+
this.#advertTtlMs = config.advertTtlMs ?? DEFAULT_ADVERT_TTL_MS;
|
|
2756
|
+
this.#heartbeatMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
2757
|
+
this.#rateLimiter = config.rateLimiter ?? new RateLimiter();
|
|
2758
|
+
this.#nonceCache = config.nonceCache ?? new NonceCache();
|
|
2759
|
+
this.#now = config.now ?? (() => Date.now());
|
|
2760
|
+
this.#resolveSenderPkFn = config.resolveSenderPk;
|
|
2761
|
+
}
|
|
2762
|
+
// ----------------------------------------------------------------
|
|
2763
|
+
// Transport interface
|
|
2764
|
+
// ----------------------------------------------------------------
|
|
2765
|
+
start() {
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Detach the transport: close every open SSE subscription, clear the advert
|
|
2769
|
+
* cache, and drop all actor / peer / inbox state. Intended for shutdown and
|
|
2770
|
+
* for test teardown.
|
|
2771
|
+
*/
|
|
2772
|
+
stop() {
|
|
2773
|
+
for (const sub of this.#broadcastSubscribers) this.#closeBroadcastSubscriber(sub);
|
|
2774
|
+
this.#broadcastSubscribers.clear();
|
|
2775
|
+
for (const inbox of this.#inboxes.values()) {
|
|
2776
|
+
for (const sub of inbox.subscribers) this.#closeInboxSubscriber(sub);
|
|
2777
|
+
inbox.subscribers.clear();
|
|
2778
|
+
}
|
|
2779
|
+
this.#inboxes.clear();
|
|
2780
|
+
this.#currentAdvert = void 0;
|
|
2781
|
+
}
|
|
2782
|
+
registerActor(did, keys) {
|
|
2783
|
+
this.#actors.set(did, { keys, handlers: /* @__PURE__ */ new Map() });
|
|
2784
|
+
}
|
|
2785
|
+
unregisterActor(did) {
|
|
2786
|
+
this.#actors.delete(did);
|
|
2787
|
+
this.#peers.delete(did);
|
|
2788
|
+
}
|
|
2789
|
+
getActorPk(did) {
|
|
2790
|
+
return this.#actors.get(did)?.keys.publicKey.compressed;
|
|
2791
|
+
}
|
|
2792
|
+
registerPeer(did, communicationPk) {
|
|
2793
|
+
try {
|
|
2794
|
+
new import_keypair2.CompressedSecp256k1PublicKey(communicationPk);
|
|
2795
|
+
} catch {
|
|
2796
|
+
throw new HttpTransportError(
|
|
2797
|
+
`Invalid peer public key for ${did}`,
|
|
2798
|
+
"INVALID_PEER_KEY",
|
|
2799
|
+
{ did, keyLength: communicationPk.length }
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
this.#peers.set(did, communicationPk);
|
|
2803
|
+
}
|
|
2804
|
+
getPeerPk(did) {
|
|
2805
|
+
return this.#peers.get(did);
|
|
2806
|
+
}
|
|
2807
|
+
registerMessageHandler(actorDid, messageType, handler) {
|
|
2808
|
+
const actor = this.#actors.get(actorDid);
|
|
2809
|
+
if (!actor) {
|
|
2810
|
+
throw new HttpTransportError(
|
|
2811
|
+
`Cannot register handler: actor ${actorDid} not registered`,
|
|
2812
|
+
"UNKNOWN_ACTOR",
|
|
2813
|
+
{ did: actorDid }
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
actor.handlers.set(messageType, handler);
|
|
2817
|
+
}
|
|
2818
|
+
unregisterMessageHandler(actorDid, messageType) {
|
|
2819
|
+
this.#actors.get(actorDid)?.handlers.delete(messageType);
|
|
2820
|
+
}
|
|
2821
|
+
async sendMessage(message, sender, recipient) {
|
|
2822
|
+
if (!recipient) {
|
|
2823
|
+
throw new HttpTransportError(
|
|
2824
|
+
"HttpServerTransport.sendMessage requires a recipient. Use publishRepeating for broadcasts.",
|
|
2825
|
+
"MISSING_RECIPIENT",
|
|
2826
|
+
{ messageType: message.type }
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
const actor = this.#actors.get(sender);
|
|
2830
|
+
if (!actor) {
|
|
2831
|
+
throw new HttpTransportError(
|
|
2832
|
+
`Unknown sender: ${sender}`,
|
|
2833
|
+
"UNKNOWN_SENDER",
|
|
2834
|
+
{ did: sender }
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
const envelope = signEnvelope(message, { did: sender, keys: actor.keys }, { to: recipient });
|
|
2838
|
+
const dataJson = JSON.stringify(envelope);
|
|
2839
|
+
const inbox = this.#getOrCreateInbox(recipient);
|
|
2840
|
+
const stored = inbox.buffer.append(SSE_EVENT.MESSAGE, dataJson);
|
|
2841
|
+
for (const sub of inbox.subscribers) {
|
|
2842
|
+
this.#safeWrite(sub.stream, stored.event, stored.data, stored.id);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
publishRepeating(message, sender, _intervalMs, _recipient) {
|
|
2846
|
+
const actor = this.#actors.get(sender);
|
|
2847
|
+
if (!actor) {
|
|
2848
|
+
throw new HttpTransportError(`Unknown sender: ${sender}`, "UNKNOWN_SENDER", { did: sender });
|
|
2849
|
+
}
|
|
2850
|
+
const envelope = signEnvelope(message, { did: sender, keys: actor.keys });
|
|
2851
|
+
const dataJson = JSON.stringify(envelope);
|
|
2852
|
+
const id = String(++this.#advertSeq);
|
|
2853
|
+
const expiresAtMs = this.#now() + this.#advertTtlMs;
|
|
2854
|
+
this.#currentAdvert = { dataJson, id, expiresAtMs };
|
|
2855
|
+
for (const sub of this.#broadcastSubscribers) {
|
|
2856
|
+
this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, dataJson, id);
|
|
2857
|
+
}
|
|
2858
|
+
return () => {
|
|
2859
|
+
if (this.#currentAdvert?.id === id) this.#currentAdvert = void 0;
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
// ----------------------------------------------------------------
|
|
2863
|
+
// Sans-I/O HTTP surface
|
|
2864
|
+
// ----------------------------------------------------------------
|
|
2865
|
+
/**
|
|
2866
|
+
* Handle a POST / GET request (non-SSE). The caller dispatches SSE paths to
|
|
2867
|
+
* {@link handleSse} instead. Returns a fully formed response; the caller's
|
|
2868
|
+
* adapter turns it into an HTTP write.
|
|
2869
|
+
*/
|
|
2870
|
+
async handleRequest(req) {
|
|
2871
|
+
const method = req.method.toUpperCase();
|
|
2872
|
+
if (method === "OPTIONS") return this.#respond(204, "", req);
|
|
2873
|
+
const path = extractPath(req.url);
|
|
2874
|
+
if (method === "GET" && path === HTTP_ROUTE.WELL_KNOWN) {
|
|
2875
|
+
return this.#respondJson(200, this.#wellKnownMetadata(), req);
|
|
2876
|
+
}
|
|
2877
|
+
if (method === "POST" && path === HTTP_ROUTE.MESSAGES) {
|
|
2878
|
+
return await this.#handleMessagesPost(req);
|
|
2879
|
+
}
|
|
2880
|
+
if (method === "POST" && path === HTTP_ROUTE.ADVERTS) {
|
|
2881
|
+
return await this.#handleAdvertsPost(req);
|
|
2882
|
+
}
|
|
2883
|
+
return this.#respondJson(404, { error: "not_found" }, req);
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Open an SSE stream for a GET request. The caller is responsible for
|
|
2887
|
+
* flushing writes and propagating the `onClose` callback when the HTTP
|
|
2888
|
+
* connection ends.
|
|
2889
|
+
*/
|
|
2890
|
+
handleSse(req, stream) {
|
|
2891
|
+
if (req.method.toUpperCase() !== "GET") {
|
|
2892
|
+
stream.close();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
const path = extractPath(req.url);
|
|
2896
|
+
if (path === HTTP_ROUTE.ADVERTS) {
|
|
2897
|
+
this.#openBroadcastSubscription(stream);
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
const inboxMatch = matchInboxPath(path);
|
|
2901
|
+
if (inboxMatch) {
|
|
2902
|
+
this.#openInboxSubscription(req, stream, inboxMatch.did, path);
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
stream.close();
|
|
2906
|
+
}
|
|
2907
|
+
// ----------------------------------------------------------------
|
|
2908
|
+
// Request handlers
|
|
2909
|
+
// ----------------------------------------------------------------
|
|
2910
|
+
async #handleMessagesPost(req) {
|
|
2911
|
+
const envelope = parseJsonBody(req.body);
|
|
2912
|
+
if (!envelope) return this.#respondJson(400, { error: "invalid_json" }, req);
|
|
2913
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
2914
|
+
if (!senderPk) {
|
|
2915
|
+
return this.#respondJson(401, { error: "unknown_sender" }, req);
|
|
2916
|
+
}
|
|
2917
|
+
try {
|
|
2918
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
this.#logger.debug("POST /v1/messages: envelope verification failed:", err);
|
|
2921
|
+
return this.#respondJson(401, { error: "invalid_envelope" }, req);
|
|
2922
|
+
}
|
|
2923
|
+
if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
|
|
2924
|
+
return this.#respondJson(409, { error: "replay" }, req);
|
|
2925
|
+
}
|
|
2926
|
+
if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
|
|
2927
|
+
return this.#respondJson(429, { error: "rate_limited" }, req);
|
|
2928
|
+
}
|
|
2929
|
+
if (!envelope.to) {
|
|
2930
|
+
return this.#respondJson(400, { error: "missing_recipient" }, req);
|
|
2931
|
+
}
|
|
2932
|
+
const actor = this.#actors.get(envelope.to);
|
|
2933
|
+
if (!actor) {
|
|
2934
|
+
return this.#respondJson(404, { error: "unknown_recipient" }, req);
|
|
2935
|
+
}
|
|
2936
|
+
const revived = reviveFromWire(envelope.message);
|
|
2937
|
+
const flat = flattenMessage(revived);
|
|
2938
|
+
const messageType = typeof flat.type === "string" ? flat.type : void 0;
|
|
2939
|
+
if (!messageType) return this.#respondJson(400, { error: "missing_message_type" }, req);
|
|
2940
|
+
const handler = actor.handlers.get(messageType);
|
|
2941
|
+
if (handler) {
|
|
2942
|
+
try {
|
|
2943
|
+
await handler(flat);
|
|
2944
|
+
} catch (err) {
|
|
2945
|
+
this.#logger.debug(`Handler threw for ${messageType}:`, err);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
return this.#respondJson(202, { ok: true }, req);
|
|
2949
|
+
}
|
|
2950
|
+
async #handleAdvertsPost(req) {
|
|
2951
|
+
const envelope = parseJsonBody(req.body);
|
|
2952
|
+
if (!envelope) return this.#respondJson(400, { error: "invalid_json" }, req);
|
|
2953
|
+
const senderPk = this.#resolveSenderPk(envelope.from);
|
|
2954
|
+
if (!senderPk) return this.#respondJson(401, { error: "unknown_sender" }, req);
|
|
2955
|
+
try {
|
|
2956
|
+
verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
|
|
2957
|
+
} catch {
|
|
2958
|
+
return this.#respondJson(401, { error: "invalid_envelope" }, req);
|
|
2959
|
+
}
|
|
2960
|
+
if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
|
|
2961
|
+
return this.#respondJson(409, { error: "replay" }, req);
|
|
2962
|
+
}
|
|
2963
|
+
if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
|
|
2964
|
+
return this.#respondJson(429, { error: "rate_limited" }, req);
|
|
2965
|
+
}
|
|
2966
|
+
if (!this.#actors.has(envelope.from)) {
|
|
2967
|
+
return this.#respondJson(403, { error: "not_an_actor" }, req);
|
|
2968
|
+
}
|
|
2969
|
+
const id = String(++this.#advertSeq);
|
|
2970
|
+
this.#currentAdvert = {
|
|
2971
|
+
dataJson: JSON.stringify(envelope),
|
|
2972
|
+
id,
|
|
2973
|
+
expiresAtMs: this.#now() + this.#advertTtlMs
|
|
2974
|
+
};
|
|
2975
|
+
for (const sub of this.#broadcastSubscribers) {
|
|
2976
|
+
this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, id);
|
|
2977
|
+
}
|
|
2978
|
+
return this.#respondJson(202, { ok: true }, req);
|
|
2979
|
+
}
|
|
2980
|
+
#openBroadcastSubscription(stream) {
|
|
2981
|
+
const sub = { stream };
|
|
2982
|
+
this.#broadcastSubscribers.add(sub);
|
|
2983
|
+
stream.onClose(() => {
|
|
2984
|
+
this.#closeBroadcastSubscriber(sub);
|
|
2985
|
+
this.#broadcastSubscribers.delete(sub);
|
|
2986
|
+
});
|
|
2987
|
+
if (this.#currentAdvert && this.#currentAdvert.expiresAtMs > this.#now()) {
|
|
2988
|
+
this.#safeWrite(stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, this.#currentAdvert.id);
|
|
2989
|
+
}
|
|
2990
|
+
if (this.#heartbeatMs > 0) {
|
|
2991
|
+
sub.heartbeatTimer = setInterval(() => {
|
|
2992
|
+
try {
|
|
2993
|
+
stream.writeComment("hb");
|
|
2994
|
+
} catch {
|
|
2995
|
+
}
|
|
2996
|
+
}, this.#heartbeatMs);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
#openInboxSubscription(req, stream, did, path) {
|
|
3000
|
+
const auth = req.headers.authorization;
|
|
3001
|
+
if (!auth) {
|
|
3002
|
+
this.#logger.debug(`Inbox subscribe: missing authorization header for ${did}`);
|
|
3003
|
+
stream.close();
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
const senderPk = this.#resolveSenderPk(did);
|
|
3007
|
+
if (!senderPk) {
|
|
3008
|
+
stream.close();
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
let parsedTs = 0;
|
|
3012
|
+
let parsedNonce = "";
|
|
3013
|
+
try {
|
|
3014
|
+
const parsed = verifyRequestAuth(auth, path, senderPk, {
|
|
3015
|
+
clockSkewSec: this.#clockSkewSec,
|
|
3016
|
+
now: () => this.#now()
|
|
3017
|
+
});
|
|
3018
|
+
if (parsed.did !== did) {
|
|
3019
|
+
stream.close();
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
parsedTs = parsed.ts;
|
|
3023
|
+
parsedNonce = parsed.nonce;
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
this.#logger.debug(`Inbox subscribe: auth verification failed for ${did}:`, err);
|
|
3026
|
+
stream.close();
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
if (!this.#nonceCache.store(did, parsedNonce, parsedTs)) {
|
|
3030
|
+
stream.close();
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (!this.#rateLimiter.consume(did, this.#now())) {
|
|
3034
|
+
stream.close();
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
const inbox = this.#getOrCreateInbox(did);
|
|
3038
|
+
const sub = { stream };
|
|
3039
|
+
inbox.subscribers.add(sub);
|
|
3040
|
+
stream.onClose(() => {
|
|
3041
|
+
this.#closeInboxSubscriber(sub);
|
|
3042
|
+
inbox.subscribers.delete(sub);
|
|
3043
|
+
});
|
|
3044
|
+
const lastEventId = req.headers["last-event-id"];
|
|
3045
|
+
for (const stored of inbox.buffer.since(lastEventId)) {
|
|
3046
|
+
this.#safeWrite(stream, stored.event, stored.data, stored.id);
|
|
3047
|
+
}
|
|
3048
|
+
if (this.#heartbeatMs > 0) {
|
|
3049
|
+
sub.heartbeatTimer = setInterval(() => {
|
|
3050
|
+
try {
|
|
3051
|
+
stream.writeComment("hb");
|
|
3052
|
+
} catch {
|
|
3053
|
+
}
|
|
3054
|
+
}, this.#heartbeatMs);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
// ----------------------------------------------------------------
|
|
3058
|
+
// Internal helpers
|
|
3059
|
+
// ----------------------------------------------------------------
|
|
3060
|
+
#getOrCreateInbox(did) {
|
|
3061
|
+
let inbox = this.#inboxes.get(did);
|
|
3062
|
+
if (!inbox) {
|
|
3063
|
+
inbox = { buffer: new InboxBuffer(this.#inboxBufferSize), subscribers: /* @__PURE__ */ new Set() };
|
|
3064
|
+
this.#inboxes.set(did, inbox);
|
|
3065
|
+
}
|
|
3066
|
+
return inbox;
|
|
3067
|
+
}
|
|
3068
|
+
#resolveSenderPk(did) {
|
|
3069
|
+
const peerBytes = this.#peers.get(did);
|
|
3070
|
+
if (peerBytes) {
|
|
3071
|
+
try {
|
|
3072
|
+
return new import_keypair2.CompressedSecp256k1PublicKey(peerBytes);
|
|
3073
|
+
} catch {
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return this.#resolveSenderPkFn?.(did);
|
|
3077
|
+
}
|
|
3078
|
+
#safeWrite(stream, event, data, id) {
|
|
3079
|
+
try {
|
|
3080
|
+
stream.writeEvent(event, data, id);
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
this.#logger.debug("SSE writeEvent failed:", err);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
#closeBroadcastSubscriber(sub) {
|
|
3086
|
+
if (sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
|
|
3087
|
+
try {
|
|
3088
|
+
sub.stream.close();
|
|
3089
|
+
} catch {
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
#closeInboxSubscriber(sub) {
|
|
3093
|
+
if (sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
|
|
3094
|
+
try {
|
|
3095
|
+
sub.stream.close();
|
|
3096
|
+
} catch {
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
#respondJson(status, body, req) {
|
|
3100
|
+
return {
|
|
3101
|
+
status,
|
|
3102
|
+
headers: { "content-type": "application/json", ...this.#corsHeaders(req) },
|
|
3103
|
+
body: JSON.stringify(body)
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
#respond(status, body, req) {
|
|
3107
|
+
return { status, headers: this.#corsHeaders(req), body };
|
|
3108
|
+
}
|
|
3109
|
+
#corsHeaders(req) {
|
|
3110
|
+
const origin = req.headers.origin;
|
|
3111
|
+
if (!origin) return {};
|
|
3112
|
+
const common = {
|
|
3113
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
3114
|
+
"access-control-allow-headers": "authorization, content-type, last-event-id",
|
|
3115
|
+
"access-control-max-age": "86400"
|
|
3116
|
+
};
|
|
3117
|
+
switch (this.#cors.mode) {
|
|
3118
|
+
case "permissive":
|
|
3119
|
+
return { "access-control-allow-origin": "*", ...common };
|
|
3120
|
+
case "allowlist":
|
|
3121
|
+
if (this.#cors.origins.includes(origin)) {
|
|
3122
|
+
return { "access-control-allow-origin": origin, vary: "origin", ...common };
|
|
3123
|
+
}
|
|
3124
|
+
return {};
|
|
3125
|
+
case "same-origin":
|
|
3126
|
+
return {};
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
#wellKnownMetadata() {
|
|
3130
|
+
return {
|
|
3131
|
+
envelopeVersion: HTTP_ENVELOPE_VERSION,
|
|
3132
|
+
heartbeatIntervalMs: this.#heartbeatMs,
|
|
3133
|
+
inboxBufferSize: this.#inboxBufferSize,
|
|
3134
|
+
advertTtlMs: this.#advertTtlMs
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
function extractPath(reqUrl) {
|
|
3139
|
+
if (reqUrl.startsWith("http://") || reqUrl.startsWith("https://")) {
|
|
3140
|
+
return new URL(reqUrl).pathname;
|
|
3141
|
+
}
|
|
3142
|
+
const q = reqUrl.indexOf("?");
|
|
3143
|
+
return q === -1 ? reqUrl : reqUrl.slice(0, q);
|
|
3144
|
+
}
|
|
3145
|
+
function matchInboxPath(path) {
|
|
3146
|
+
if (!path.startsWith(INBOX_PATH_PREFIX) || !path.endsWith(INBOX_PATH_SUFFIX)) return void 0;
|
|
3147
|
+
const encodedDid = path.slice(INBOX_PATH_PREFIX.length, path.length - INBOX_PATH_SUFFIX.length);
|
|
3148
|
+
if (!encodedDid) return void 0;
|
|
3149
|
+
try {
|
|
3150
|
+
return { did: decodeURIComponent(encodedDid) };
|
|
3151
|
+
} catch {
|
|
3152
|
+
return void 0;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
function parseJsonBody(body) {
|
|
3156
|
+
if (body === void 0 || body === "") return void 0;
|
|
3157
|
+
try {
|
|
3158
|
+
return JSON.parse(body);
|
|
3159
|
+
} catch {
|
|
3160
|
+
return void 0;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function flattenMessage(msg) {
|
|
3164
|
+
if (msg.body && typeof msg.body === "object") {
|
|
3165
|
+
return { ...msg, ...msg.body };
|
|
3166
|
+
}
|
|
3167
|
+
return msg;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/service/sse-writer.ts
|
|
3171
|
+
function formatSseEvent(event, data, id) {
|
|
3172
|
+
const lines = [];
|
|
3173
|
+
if (id !== void 0) lines.push(`id: ${id}`);
|
|
3174
|
+
lines.push(`event: ${event}`);
|
|
3175
|
+
for (const part of data.split("\n")) lines.push(`data: ${part}`);
|
|
3176
|
+
lines.push("");
|
|
3177
|
+
lines.push("");
|
|
3178
|
+
return lines.join("\n");
|
|
3179
|
+
}
|
|
3180
|
+
function formatSseComment(comment) {
|
|
3181
|
+
const safe = comment.replace(/\n/g, " ");
|
|
3182
|
+
return `: ${safe}
|
|
3183
|
+
|
|
3184
|
+
`;
|
|
3185
|
+
}
|
|
3186
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3187
|
+
0 && (module.exports = {
|
|
3188
|
+
AggregationService,
|
|
3189
|
+
AggregationServiceRunner,
|
|
3190
|
+
DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
3191
|
+
DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
3192
|
+
HttpServerTransport,
|
|
3193
|
+
InMemoryRateLimitStore,
|
|
3194
|
+
InboxBuffer,
|
|
3195
|
+
NonceCache,
|
|
3196
|
+
RateLimiter,
|
|
3197
|
+
formatSseComment,
|
|
3198
|
+
formatSseEvent
|
|
3199
|
+
});
|