@did-btcr2/method 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +1371 -260
- package/dist/browser.mjs +1371 -260
- package/dist/cjs/index.js +1464 -343
- package/dist/esm/core/aggregation/beacon-strategy.js +19 -6
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -1
- package/dist/esm/core/aggregation/cohort.js +142 -33
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/conditions.js +27 -0
- package/dist/esm/core/aggregation/conditions.js.map +1 -1
- package/dist/esm/core/aggregation/fallback-spend.js +132 -0
- package/dist/esm/core/aggregation/fallback-spend.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +46 -1
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
- package/dist/esm/core/aggregation/messages/constants.js +11 -0
- package/dist/esm/core/aggregation/messages/constants.js.map +1 -1
- package/dist/esm/core/aggregation/messages/factories.js +27 -1
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/guards.js +4 -1
- package/dist/esm/core/aggregation/messages/guards.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +218 -18
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/phases.js +6 -0
- package/dist/esm/core/aggregation/phases.js.map +1 -1
- package/dist/esm/core/aggregation/recovery-policy.js +156 -0
- package/dist/esm/core/aggregation/recovery-policy.js.map +1 -0
- package/dist/esm/core/aggregation/recovery-spend.js +106 -0
- package/dist/esm/core/aggregation/recovery-spend.js.map +1 -0
- package/dist/esm/core/aggregation/runner/aggregation-runner.js +11 -3
- package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +106 -19
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +409 -195
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +221 -11
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +3 -3
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/envelope.js +3 -3
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +1 -1
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +1 -1
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +1 -1
- package/dist/esm/core/aggregation/transport/http/request-auth.js +1 -1
- package/dist/esm/core/aggregation/transport/http/server.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +1 -1
- package/dist/esm/core/aggregation/transport/in-memory.js +1 -1
- package/dist/esm/core/aggregation/transport/nostr.js +6 -6
- package/dist/esm/core/beacon/beacon.js +15 -10
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +6 -6
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +2 -2
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +3 -3
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/resolver.js +8 -8
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +6 -6
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +2 -2
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +8 -4
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -1
- package/dist/types/core/aggregation/cohort.d.ts +88 -17
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/conditions.d.ts +28 -0
- package/dist/types/core/aggregation/conditions.d.ts.map +1 -1
- package/dist/types/core/aggregation/fallback-spend.d.ts +94 -0
- package/dist/types/core/aggregation/fallback-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/logger.d.ts +1 -1
- package/dist/types/core/aggregation/messages/base.d.ts +6 -0
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +50 -2
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/constants.d.ts +8 -0
- package/dist/types/core/aggregation/messages/constants.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/factories.d.ts +40 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/guards.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +47 -4
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/phases.d.ts +8 -2
- package/dist/types/core/aggregation/phases.d.ts.map +1 -1
- package/dist/types/core/aggregation/recovery-policy.d.ts +132 -0
- package/dist/types/core/aggregation/recovery-policy.d.ts.map +1 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts +94 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +13 -3
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +48 -16
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +23 -14
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +102 -23
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +26 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +2 -2
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +3 -3
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/server.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +2 -2
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/in-memory.d.ts +1 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +7 -7
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +2 -2
- package/dist/types/core/beacon/interfaces.d.ts +1 -1
- package/dist/types/core/resolver.d.ts +3 -3
- package/dist/types/core/updater.d.ts +4 -4
- package/dist/types/did-btcr2.d.ts +2 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/beacon-strategy.ts +27 -9
- package/src/core/aggregation/cohort.ts +171 -35
- package/src/core/aggregation/conditions.ts +56 -0
- package/src/core/aggregation/fallback-spend.ts +223 -0
- package/src/core/aggregation/logger.ts +1 -1
- package/src/core/aggregation/messages/base.ts +6 -0
- package/src/core/aggregation/messages/bodies.ts +94 -1
- package/src/core/aggregation/messages/constants.ts +12 -0
- package/src/core/aggregation/messages/factories.ts +57 -0
- package/src/core/aggregation/messages/guards.ts +6 -0
- package/src/core/aggregation/participant.ts +264 -20
- package/src/core/aggregation/phases.ts +9 -0
- package/src/core/aggregation/recovery-policy.ts +237 -0
- package/src/core/aggregation/recovery-spend.ts +192 -0
- package/src/core/aggregation/runner/aggregation-runner.ts +27 -5
- package/src/core/aggregation/runner/events.ts +47 -19
- package/src/core/aggregation/runner/participant-runner.ts +113 -31
- package/src/core/aggregation/runner/service-runner.ts +484 -203
- package/src/core/aggregation/service.ts +260 -14
- package/src/core/aggregation/signing-session.ts +3 -3
- package/src/core/aggregation/transport/http/envelope.ts +3 -3
- package/src/core/aggregation/transport/http/inbox-buffer.ts +1 -1
- package/src/core/aggregation/transport/http/nonce-cache.ts +1 -1
- package/src/core/aggregation/transport/http/rate-limiter.ts +1 -1
- package/src/core/aggregation/transport/http/request-auth.ts +1 -1
- package/src/core/aggregation/transport/http/server.ts +1 -1
- package/src/core/aggregation/transport/http/sse-stream.ts +2 -2
- package/src/core/aggregation/transport/http/sse-writer.ts +1 -1
- package/src/core/aggregation/transport/in-memory.ts +1 -1
- package/src/core/aggregation/transport/nostr.ts +6 -6
- package/src/core/aggregation/transport/transport.ts +4 -4
- package/src/core/beacon/beacon.ts +16 -11
- package/src/core/beacon/cas-beacon.ts +7 -7
- package/src/core/beacon/interfaces.ts +1 -1
- package/src/core/beacon/singleton-beacon.ts +2 -2
- package/src/core/beacon/smt-beacon.ts +3 -3
- package/src/core/resolver.ts +8 -8
- package/src/core/updater.ts +6 -6
- package/src/did-btcr2.ts +2 -2
- package/src/index.ts +3 -0
|
@@ -2,18 +2,24 @@ import { canonicalize } from '@did-btcr2/common';
|
|
|
2
2
|
import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
|
|
3
3
|
import { BIP340Cryptosuite, SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
4
4
|
import type { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
5
|
-
import {
|
|
5
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
6
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
6
7
|
import type { Transaction } from '@scure/btc-signer';
|
|
7
8
|
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
8
9
|
import { AggregationCohort } from './cohort.js';
|
|
9
10
|
import { validateCohortConditions, type CohortConditions } from './conditions.js';
|
|
10
11
|
import { AggregationServiceError } from './errors.js';
|
|
12
|
+
import { buildFallbackSpend, fallbackSighash } from './fallback-spend.js';
|
|
13
|
+
import type { FallbackSignature } from './fallback-spend.js';
|
|
14
|
+
import { buildFallbackLeaf } from './recovery-policy.js';
|
|
11
15
|
import type { BaseMessage } from './messages/base.js';
|
|
12
16
|
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
13
17
|
import {
|
|
14
18
|
COHORT_OPT_IN,
|
|
19
|
+
FALLBACK_SIGNATURE,
|
|
15
20
|
NONCE_CONTRIBUTION,
|
|
16
21
|
SIGNATURE_AUTHORIZATION,
|
|
22
|
+
SUBMIT_NONINCLUDED,
|
|
17
23
|
SUBMIT_UPDATE,
|
|
18
24
|
VALIDATION_ACK,
|
|
19
25
|
} from './messages/constants.js';
|
|
@@ -24,6 +30,7 @@ import {
|
|
|
24
30
|
createCohortOptInAcceptMessage,
|
|
25
31
|
createCohortReadyMessage,
|
|
26
32
|
createDistributeAggregatedDataMessage,
|
|
33
|
+
createFallbackAuthorizationRequestMessage,
|
|
27
34
|
} from './messages/factories.js';
|
|
28
35
|
import type { ServiceCohortPhaseType } from './phases.js';
|
|
29
36
|
import { ServiceCohortPhase } from './phases.js';
|
|
@@ -58,8 +65,19 @@ export interface ValidationProgress {
|
|
|
58
65
|
/** Final aggregation result for a cohort. */
|
|
59
66
|
export interface AggregationResult {
|
|
60
67
|
cohortId: string;
|
|
68
|
+
/**
|
|
69
|
+
* The 64-byte aggregated MuSig2 signature for the optimistic key path. Empty
|
|
70
|
+
* for a `script-path` result: the k-of-n fallback embeds k separate signatures
|
|
71
|
+
* directly in the witness of {@link signedTx}, with no single aggregate sig.
|
|
72
|
+
*/
|
|
61
73
|
signature: Uint8Array;
|
|
62
74
|
signedTx: Transaction;
|
|
75
|
+
/**
|
|
76
|
+
* Which Taproot spend path produced {@link signedTx}: `key-path` is the
|
|
77
|
+
* optimistic n-of-n MuSig2 spend, `script-path` is the k-of-n fallback (ADR
|
|
78
|
+
* 042). Absent is treated as `key-path` for backward compatibility.
|
|
79
|
+
*/
|
|
80
|
+
path?: 'key-path' | 'script-path';
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
/** Transaction data needed to start a signing session. */
|
|
@@ -86,7 +104,7 @@ export interface Rejection {
|
|
|
86
104
|
reason: string;
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
/** Per-cohort service state
|
|
107
|
+
/** Per-cohort service state - internal. */
|
|
90
108
|
interface ServiceCohortState {
|
|
91
109
|
phase: ServiceCohortPhaseType;
|
|
92
110
|
cohort: AggregationCohort;
|
|
@@ -95,6 +113,12 @@ interface ServiceCohortState {
|
|
|
95
113
|
acceptedParticipants: Set<string>;
|
|
96
114
|
signingSession?: BeaconSigningSession;
|
|
97
115
|
result?: AggregationResult;
|
|
116
|
+
/**
|
|
117
|
+
* Collected fallback (k-of-n script-path) signatures, keyed by signer DID.
|
|
118
|
+
* Populated only after {@link AggregationService.startFallbackSigning}. Each is
|
|
119
|
+
* a verified standalone BIP-340 signature over the fallback script-path sighash.
|
|
120
|
+
*/
|
|
121
|
+
fallbackSignatures?: Map<string, FallbackSignature>;
|
|
98
122
|
/** Rejections accumulated since last drain. Runner polls via drainRejections(). */
|
|
99
123
|
rejections: Array<Rejection>;
|
|
100
124
|
}
|
|
@@ -147,7 +171,7 @@ export class AggregationService {
|
|
|
147
171
|
|
|
148
172
|
receive(message: BaseMessage): void {
|
|
149
173
|
// Reject messages whose wire version doesn't match what this build speaks.
|
|
150
|
-
// Missing version
|
|
174
|
+
// Missing version, treat as legacy and drop: bumping the protocol must be
|
|
151
175
|
// coordinated across all participants.
|
|
152
176
|
const version = message.version;
|
|
153
177
|
if(version === undefined || version !== AGGREGATION_WIRE_VERSION) {
|
|
@@ -171,6 +195,9 @@ export class AggregationService {
|
|
|
171
195
|
case SUBMIT_UPDATE:
|
|
172
196
|
this.#handleSubmitUpdate(message);
|
|
173
197
|
break;
|
|
198
|
+
case SUBMIT_NONINCLUDED:
|
|
199
|
+
this.#handleSubmitNonInclusion(message);
|
|
200
|
+
break;
|
|
174
201
|
case VALIDATION_ACK:
|
|
175
202
|
this.#handleValidationAck(message);
|
|
176
203
|
break;
|
|
@@ -180,8 +207,11 @@ export class AggregationService {
|
|
|
180
207
|
case SIGNATURE_AUTHORIZATION:
|
|
181
208
|
this.#handleSignatureAuthorization(message);
|
|
182
209
|
break;
|
|
210
|
+
case FALLBACK_SIGNATURE:
|
|
211
|
+
this.#handleFallbackSignature(message);
|
|
212
|
+
break;
|
|
183
213
|
default:
|
|
184
|
-
// Unknown message type
|
|
214
|
+
// Unknown message type - silently ignore
|
|
185
215
|
break;
|
|
186
216
|
}
|
|
187
217
|
}
|
|
@@ -202,7 +232,7 @@ export class AggregationService {
|
|
|
202
232
|
|
|
203
233
|
/**
|
|
204
234
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
205
|
-
* Cohort starts in `Created` phase
|
|
235
|
+
* Cohort starts in `Created` phase - call `advertise()` to broadcast.
|
|
206
236
|
*/
|
|
207
237
|
createCohort(config: CohortConfig): string {
|
|
208
238
|
// Fail fast on invalid conditions rather than discovering them at finalize.
|
|
@@ -214,10 +244,14 @@ export class AggregationService {
|
|
|
214
244
|
);
|
|
215
245
|
}
|
|
216
246
|
const cohort = new AggregationCohort({
|
|
217
|
-
serviceDid
|
|
218
|
-
minParticipants
|
|
219
|
-
network
|
|
220
|
-
beaconType
|
|
247
|
+
serviceDid : this.did,
|
|
248
|
+
minParticipants : config.minParticipants,
|
|
249
|
+
network : config.network,
|
|
250
|
+
beaconType : config.beaconType,
|
|
251
|
+
recoveryKey : hexToBytes(config.recoveryKey),
|
|
252
|
+
recoverySequence : config.recoverySequence,
|
|
253
|
+
fundingModel : config.fundingModel,
|
|
254
|
+
fallbackThreshold : config.fallbackThreshold,
|
|
221
255
|
});
|
|
222
256
|
this.#cohortStates.set(cohort.id, {
|
|
223
257
|
phase : ServiceCohortPhase.Created,
|
|
@@ -289,7 +323,7 @@ export class AggregationService {
|
|
|
289
323
|
|
|
290
324
|
// Reject re-opt-in from already-accepted participants. Without this guard a
|
|
291
325
|
// participant could send a second opt-in with a different key, overwriting
|
|
292
|
-
// pendingOptIns[did] while cohortKeys still holds the original key
|
|
326
|
+
// pendingOptIns[did] while cohortKeys still holds the original key - opening
|
|
293
327
|
// a desync window where #verifySubmittedUpdate accepts updates signed with
|
|
294
328
|
// a key that is NOT in the MuSig2 cohort.
|
|
295
329
|
if(state.acceptedParticipants.has(participantDid)) return;
|
|
@@ -427,7 +461,7 @@ export class AggregationService {
|
|
|
427
461
|
|
|
428
462
|
// Cap the canonicalized update size before doing any heavier verification
|
|
429
463
|
// work. Without this guard, a participant could submit multi-MB payloads
|
|
430
|
-
// that the service would canonicalize, hash, and aggregate
|
|
464
|
+
// that the service would canonicalize, hash, and aggregate - cheap DoS.
|
|
431
465
|
const canonicalSize = canonicalize(signedUpdate as unknown as Record<string, unknown>).length;
|
|
432
466
|
if(canonicalSize > this.maxUpdateSizeBytes) {
|
|
433
467
|
state.rejections.push({
|
|
@@ -451,12 +485,81 @@ export class AggregationService {
|
|
|
451
485
|
return;
|
|
452
486
|
}
|
|
453
487
|
|
|
488
|
+
// One response per round. A member that already declined cannot also submit,
|
|
489
|
+
// and a member that already submitted cannot resubmit (a silent overwrite
|
|
490
|
+
// would corrupt the aggregated data the member already validated against).
|
|
491
|
+
// Both are dropped as rejections, symmetric with #handleSubmitNonInclusion.
|
|
492
|
+
if(state.cohort.nonIncluded.has(message.from)) {
|
|
493
|
+
state.rejections.push({
|
|
494
|
+
from : message.from,
|
|
495
|
+
code : 'UPDATE_MALFORMED',
|
|
496
|
+
reason : 'Participant already declined this round; cannot also submit an update',
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if(state.cohort.pendingUpdates.has(message.from)) {
|
|
501
|
+
state.rejections.push({
|
|
502
|
+
from : message.from,
|
|
503
|
+
code : 'UPDATE_MALFORMED',
|
|
504
|
+
reason : 'Participant already submitted an update this round; cannot resubmit',
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
454
509
|
state.cohort.addUpdate(message.from, signedUpdate);
|
|
455
510
|
|
|
456
511
|
if(state.phase === ServiceCohortPhase.CohortSet) {
|
|
457
512
|
state.phase = ServiceCohortPhase.CollectingUpdates;
|
|
458
513
|
}
|
|
459
|
-
if(state.cohort.
|
|
514
|
+
if(state.cohort.hasAllResponses()) {
|
|
515
|
+
state.phase = ServiceCohortPhase.UpdatesCollected;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
|
|
521
|
+
* update this round (cooperative non-inclusion). Membership is proven by the
|
|
522
|
+
* signed transport envelope, so the body carries only the cohortId. The member
|
|
523
|
+
* stays in the cohort and still signs; it is absent from the CAS map and
|
|
524
|
+
* carries a non-inclusion leaf in the SMT.
|
|
525
|
+
*/
|
|
526
|
+
#handleSubmitNonInclusion(message: BaseMessage): void {
|
|
527
|
+
const cohortId = message.body?.cohortId;
|
|
528
|
+
if(!cohortId) return;
|
|
529
|
+
const state = this.#cohortStates.get(cohortId);
|
|
530
|
+
if(!state) return;
|
|
531
|
+
if(state.phase !== ServiceCohortPhase.CohortSet && state.phase !== ServiceCohortPhase.CollectingUpdates) return;
|
|
532
|
+
|
|
533
|
+
// Membership is proven by the signed envelope sender. A non-member decline is
|
|
534
|
+
// dropped as a rejection, never thrown: addNonInclusion would otherwise throw
|
|
535
|
+
// UNKNOWN_PARTICIPANT out of receive() and fail the whole cohort, a DoS any
|
|
536
|
+
// non-member could trigger. Mirrors the SUBMIT_UPDATE verification path.
|
|
537
|
+
if(!state.cohort.participants.includes(message.from)) {
|
|
538
|
+
state.rejections.push({
|
|
539
|
+
from : message.from,
|
|
540
|
+
code : 'UPDATE_MALFORMED',
|
|
541
|
+
reason : 'Sender is not a member of this cohort',
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// One response per round: already submitted or already declined. Surface the
|
|
547
|
+
// conflict as a rejection, symmetric with the double-submit guard above.
|
|
548
|
+
if(state.cohort.pendingUpdates.has(message.from) || state.cohort.nonIncluded.has(message.from)) {
|
|
549
|
+
state.rejections.push({
|
|
550
|
+
from : message.from,
|
|
551
|
+
code : 'UPDATE_MALFORMED',
|
|
552
|
+
reason : 'Participant already responded this round',
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
state.cohort.addNonInclusion(message.from);
|
|
558
|
+
|
|
559
|
+
if(state.phase === ServiceCohortPhase.CohortSet) {
|
|
560
|
+
state.phase = ServiceCohortPhase.CollectingUpdates;
|
|
561
|
+
}
|
|
562
|
+
if(state.cohort.hasAllResponses()) {
|
|
460
563
|
state.phase = ServiceCohortPhase.UpdatesCollected;
|
|
461
564
|
}
|
|
462
565
|
}
|
|
@@ -588,7 +691,7 @@ export class AggregationService {
|
|
|
588
691
|
|
|
589
692
|
/**
|
|
590
693
|
* Start a signing session by creating auth requests for all participants.
|
|
591
|
-
* The caller provides the transaction data
|
|
694
|
+
* The caller provides the transaction data - typically built via
|
|
592
695
|
* `buildBeaconTransaction()` against a Bitcoin connection.
|
|
593
696
|
*/
|
|
594
697
|
startSigning(cohortId: string, txData: SigningTxData): BaseMessage[] {
|
|
@@ -703,7 +806,7 @@ export class AggregationService {
|
|
|
703
806
|
state.signingSession.addPartialSignature(message.from, partialSignature);
|
|
704
807
|
|
|
705
808
|
if(state.signingSession.partialSignatures.size === state.cohort.participants.length) {
|
|
706
|
-
// All partial sigs received
|
|
809
|
+
// All partial sigs received - generate final signature
|
|
707
810
|
const signature = state.signingSession.generateFinalSignature();
|
|
708
811
|
|
|
709
812
|
// Set Taproot key-path witness (finalScriptWitness injects the aggregated MuSig2 sig)
|
|
@@ -713,12 +816,155 @@ export class AggregationService {
|
|
|
713
816
|
cohortId,
|
|
714
817
|
signature,
|
|
715
818
|
signedTx : state.signingSession.pendingTx,
|
|
819
|
+
path : 'key-path',
|
|
716
820
|
};
|
|
717
821
|
state.phase = ServiceCohortPhase.Complete;
|
|
718
822
|
}
|
|
719
823
|
}
|
|
720
824
|
|
|
721
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Abandon the optimistic n-of-n key path and ask members to authorize the
|
|
828
|
+
* k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
|
|
829
|
+
* liveness, ADR 042). Reuses the in-flight signing session's transaction and
|
|
830
|
+
* spent output, so the announcement and its outputs are unchanged: only the
|
|
831
|
+
* witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
|
|
832
|
+
* participant.
|
|
833
|
+
*
|
|
834
|
+
* Callable once optimistic signing has started (the session and its tx exist)
|
|
835
|
+
* and before it completes. A cohort can take exactly one of the two paths: the
|
|
836
|
+
* caller (runner) must commit to fallback and stop driving the optimistic path.
|
|
837
|
+
*/
|
|
838
|
+
startFallbackSigning(cohortId: string): BaseMessage[] {
|
|
839
|
+
const state = this.#cohortStates.get(cohortId);
|
|
840
|
+
if(!state) {
|
|
841
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, 'COHORT_NOT_FOUND', { cohortId });
|
|
842
|
+
}
|
|
843
|
+
if(!state.signingSession) {
|
|
844
|
+
throw new AggregationServiceError(
|
|
845
|
+
`Cannot start fallback for cohort ${cohortId}: no signing session.`,
|
|
846
|
+
'NO_SIGNING_SESSION', { cohortId }
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const signingPhases: ServiceCohortPhaseType[] = [
|
|
850
|
+
ServiceCohortPhase.SigningStarted,
|
|
851
|
+
ServiceCohortPhase.NoncesCollected,
|
|
852
|
+
ServiceCohortPhase.AwaitingPartialSigs,
|
|
853
|
+
];
|
|
854
|
+
if(!signingPhases.includes(state.phase)) {
|
|
855
|
+
throw new AggregationServiceError(
|
|
856
|
+
`Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
857
|
+
'INVALID_PHASE', { cohortId, phase: state.phase }
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const session = state.signingSession;
|
|
862
|
+
const prevOutScript = session.prevOutScripts[0];
|
|
863
|
+
const prevOutValue = session.prevOutValues[0];
|
|
864
|
+
if(!prevOutScript || prevOutValue === undefined) {
|
|
865
|
+
throw new AggregationServiceError(
|
|
866
|
+
`Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
|
|
867
|
+
'MISSING_PREV_OUT', { cohortId }
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
872
|
+
cohortKeys : state.cohort.cohortKeys,
|
|
873
|
+
fallbackThreshold : state.cohort.effectiveFallbackThreshold,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
state.fallbackSignatures = new Map();
|
|
877
|
+
state.phase = ServiceCohortPhase.FallbackRequested;
|
|
878
|
+
|
|
879
|
+
const messages: BaseMessage[] = [];
|
|
880
|
+
for(const participantDid of state.cohort.participants) {
|
|
881
|
+
messages.push(createFallbackAuthorizationRequestMessage({
|
|
882
|
+
from : this.did,
|
|
883
|
+
to : participantDid,
|
|
884
|
+
cohortId,
|
|
885
|
+
sessionId : session.id,
|
|
886
|
+
pendingTx : session.pendingTx.hex,
|
|
887
|
+
prevOutScriptHex : bytesToHex(prevOutScript),
|
|
888
|
+
prevOutValue : prevOutValue.toString(),
|
|
889
|
+
fallbackLeafScriptHex : bytesToHex(fallbackLeaf),
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
return messages;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
|
|
897
|
+
* signature over the fallback script-path sighash. The signature is
|
|
898
|
+
* authenticated to the sender (its `signerPk` must be the sender's own cohort
|
|
899
|
+
* key and the signature must verify against the sighash) and collected. Once k
|
|
900
|
+
* valid signatures are in, the k-of-n fallback spend is assembled and the
|
|
901
|
+
* cohort completes via the script path.
|
|
902
|
+
*/
|
|
903
|
+
#handleFallbackSignature(message: BaseMessage): void {
|
|
904
|
+
const cohortId = message.body?.cohortId;
|
|
905
|
+
if(!cohortId) return;
|
|
906
|
+
const state = this.#cohortStates.get(cohortId);
|
|
907
|
+
if(!state || !state.signingSession || !state.fallbackSignatures) return;
|
|
908
|
+
if(state.phase !== ServiceCohortPhase.FallbackRequested) return;
|
|
909
|
+
|
|
910
|
+
const sessionId = message.body?.sessionId;
|
|
911
|
+
if(sessionId !== state.signingSession.id) return;
|
|
912
|
+
|
|
913
|
+
const signerPk = message.body?.signerPk as Uint8Array | undefined;
|
|
914
|
+
const fallbackSignature = message.body?.fallbackSignature as Uint8Array | undefined;
|
|
915
|
+
if(!signerPk || !fallbackSignature) return;
|
|
916
|
+
|
|
917
|
+
const prevOutScript = state.signingSession.prevOutScripts[0];
|
|
918
|
+
const prevOutValue = state.signingSession.prevOutValues[0];
|
|
919
|
+
if(!prevOutScript || prevOutValue === undefined) return;
|
|
920
|
+
|
|
921
|
+
// Authenticate the signature to the sender: signerPk must be the sender's own
|
|
922
|
+
// cohort key (x-only). A non-member or a mismatched key is dropped as a
|
|
923
|
+
// rejection, never thrown, so one bad contribution cannot stall the fallback.
|
|
924
|
+
const memberKey = state.cohort.participantKeys.get(message.from);
|
|
925
|
+
if(!memberKey) {
|
|
926
|
+
state.rejections.push({ from: message.from, code: 'UPDATE_MALFORMED', reason: 'Fallback signature from a non-member' });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const memberXOnly = memberKey.slice(1);
|
|
930
|
+
if(signerPk.length !== 32 || !memberXOnly.every((b, i) => b === signerPk[i])) {
|
|
931
|
+
state.rejections.push({ from: message.from, code: 'UPDATE_MALFORMED', reason: 'Fallback signerPk does not match the sender cohort key' });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
936
|
+
cohortKeys : state.cohort.cohortKeys,
|
|
937
|
+
fallbackThreshold : state.cohort.effectiveFallbackThreshold,
|
|
938
|
+
});
|
|
939
|
+
const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
940
|
+
let valid = false;
|
|
941
|
+
try { valid = fallbackSignature.length === 64 && schnorr.verify(fallbackSignature, sighash, signerPk); } catch { valid = false; }
|
|
942
|
+
if(!valid) {
|
|
943
|
+
state.rejections.push({ from: message.from, code: 'UPDATE_VERIFICATION_FAILED', reason: 'Fallback signature failed verification' });
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
state.fallbackSignatures.set(message.from, { pubKey: signerPk, signature: fallbackSignature });
|
|
948
|
+
|
|
949
|
+
if(state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
|
|
950
|
+
const signedTx = buildFallbackSpend({
|
|
951
|
+
pendingTx : state.signingSession.pendingTx,
|
|
952
|
+
cohortKeys : state.cohort.cohortKeys,
|
|
953
|
+
fallbackThreshold : state.cohort.effectiveFallbackThreshold,
|
|
954
|
+
recoveryKey : state.cohort.recoveryKey!,
|
|
955
|
+
recoverySequence : state.cohort.recoverySequence!,
|
|
956
|
+
fundingModel : state.cohort.fundingModel,
|
|
957
|
+
network : state.cohort.network,
|
|
958
|
+
prevOutScript,
|
|
959
|
+
prevOutValue,
|
|
960
|
+
signatures : [ ...state.fallbackSignatures.values() ],
|
|
961
|
+
});
|
|
962
|
+
state.result = { cohortId, signature: new Uint8Array(), signedTx, path: 'script-path' };
|
|
963
|
+
state.phase = ServiceCohortPhase.Complete;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
|
|
722
968
|
getResult(cohortId: string): AggregationResult | undefined {
|
|
723
969
|
return this.#cohortStates.get(cohortId)?.result;
|
|
724
970
|
}
|
|
@@ -44,13 +44,13 @@ export class BeaconSigningSession {
|
|
|
44
44
|
/** Previous output values for Taproot sighash computation. */
|
|
45
45
|
public prevOutValues: bigint[];
|
|
46
46
|
|
|
47
|
-
/** Map of participant publicKey-hex
|
|
47
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
48
48
|
public nonceContributions: Map<PublicKeyHex, Nonce> = new Map();
|
|
49
49
|
|
|
50
50
|
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
51
51
|
public aggregatedNonce?: Uint8Array;
|
|
52
52
|
|
|
53
|
-
/** Map of participant DID
|
|
53
|
+
/** Map of participant DID to partial signature. */
|
|
54
54
|
public partialSignatures: Map<string, Uint8Array> = new Map();
|
|
55
55
|
|
|
56
56
|
/** Final 64-byte Schnorr signature. */
|
|
@@ -251,7 +251,7 @@ export class BeaconSigningSession {
|
|
|
251
251
|
throw new SigningSessionError('Aggregated nonce not available.', 'MISSING_AGGREGATED_NONCE');
|
|
252
252
|
}
|
|
253
253
|
if(!this.#secretNonce) {
|
|
254
|
-
throw new SigningSessionError('Secret nonce not available
|
|
254
|
+
throw new SigningSessionError('Secret nonce not available - generateNonceContribution() must be called first.', 'MISSING_SECRET_NONCE');
|
|
255
255
|
}
|
|
256
256
|
const session = new musig2.Session(
|
|
257
257
|
this.aggregatedNonce,
|
|
@@ -39,7 +39,7 @@ export interface VerifyEnvelopeOptions {
|
|
|
39
39
|
/**
|
|
40
40
|
* Build a {@link SignedEnvelope} around `message`.
|
|
41
41
|
*
|
|
42
|
-
* Pure function
|
|
42
|
+
* Pure function - no I/O beyond `randomBytes` for nonce generation (which
|
|
43
43
|
* uses the platform's cryptographic RNG: `crypto.getRandomValues` in browsers,
|
|
44
44
|
* `node:crypto` in Node). Deterministic when both `nonce` and `timestamp` are
|
|
45
45
|
* supplied via {@link SignEnvelopeOptions}.
|
|
@@ -73,7 +73,7 @@ export function signEnvelope(
|
|
|
73
73
|
* communication public key. Throws {@link HttpTransportError} on any failure;
|
|
74
74
|
* returns normally on success.
|
|
75
75
|
*
|
|
76
|
-
* Does NOT check nonce uniqueness
|
|
76
|
+
* Does NOT check nonce uniqueness - replay protection is the caller's
|
|
77
77
|
* responsibility (the server-side transport maintains an LRU cache).
|
|
78
78
|
*/
|
|
79
79
|
export function verifyEnvelope(
|
|
@@ -161,7 +161,7 @@ function normalizeMessage(message: EnvelopeMessage): Record<string, unknown> {
|
|
|
161
161
|
*
|
|
162
162
|
* Without this, `JSON.stringify` serializes a `Uint8Array` as an index-keyed
|
|
163
163
|
* object (`{"0":1,"1":2,...}`), which `canonicalize` then re-parses into a
|
|
164
|
-
* plain object
|
|
164
|
+
* plain object - the receiver cannot reconstruct the original bytes even
|
|
165
165
|
* though the signature still verifies.
|
|
166
166
|
*/
|
|
167
167
|
export function normalizeForWire(value: unknown): unknown {
|
|
@@ -13,7 +13,7 @@ export interface StoredEvent {
|
|
|
13
13
|
* When a subscriber (re)connects with a `Last-Event-ID` header, the server
|
|
14
14
|
* uses {@link since} to replay everything that arrived while the subscriber
|
|
15
15
|
* was disconnected. Events older than the replay window (evicted from the
|
|
16
|
-
* ring) are unrecoverable
|
|
16
|
+
* ring) are unrecoverable - callers should choose `capacity` based on
|
|
17
17
|
* expected message rate × acceptable reconnect window.
|
|
18
18
|
*/
|
|
19
19
|
export class InboxBuffer {
|
|
@@ -6,7 +6,7 @@ export interface NonceCacheConfig {
|
|
|
6
6
|
/**
|
|
7
7
|
* Bounded anti-replay cache for `(did, nonce)` pairs.
|
|
8
8
|
*
|
|
9
|
-
* Replay windowing is the caller's responsibility
|
|
9
|
+
* Replay windowing is the caller's responsibility - this cache only detects
|
|
10
10
|
* duplicates. Callers are expected to reject envelopes/headers whose timestamp
|
|
11
11
|
* is outside the clock-skew window *before* consulting the cache, so entries
|
|
12
12
|
* here are always within the protocol's acceptable window.
|
|
@@ -39,7 +39,7 @@ export interface RateLimiterConfig {
|
|
|
39
39
|
* sender DID). Tokens refill linearly at `rps` up to `burst`. Each `consume`
|
|
40
40
|
* call atomically debits one token or returns `false` to reject.
|
|
41
41
|
*
|
|
42
|
-
* The limiter is synchronous and deterministic given `nowMs`
|
|
42
|
+
* The limiter is synchronous and deterministic given `nowMs` - tests can
|
|
43
43
|
* drive it with a fixed clock to exercise exact boundaries.
|
|
44
44
|
*/
|
|
45
45
|
export class RateLimiter {
|
|
@@ -61,7 +61,7 @@ export function buildRequestAuth(
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Parse a `BTCR2-Sig` auth header value into its structured fields. Does NOT
|
|
64
|
-
* verify the signature
|
|
64
|
+
* verify the signature: call {@link verifyRequestAuth} for that.
|
|
65
65
|
*/
|
|
66
66
|
export function parseRequestAuth(headerValue: string): ParsedRequestAuth {
|
|
67
67
|
const prefix = `${REQUEST_AUTH_SCHEME} `;
|
|
@@ -114,7 +114,7 @@ const DEFAULT_ADVERT_TTL_MS = 5 * 60 * 1000;
|
|
|
114
114
|
const DEFAULT_HEARTBEAT_MS = 20_000;
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Server-side HTTP transport. Sans-I/O
|
|
117
|
+
* Server-side HTTP transport. Sans-I/O - the caller mounts
|
|
118
118
|
* {@link handleRequest} and {@link handleSse} under their HTTP framework of
|
|
119
119
|
* choice; the transport owns only in-memory state (actors, inboxes, advert
|
|
120
120
|
* cache, replay / rate-limit policies).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parsed Server-Sent Events record.
|
|
3
3
|
*
|
|
4
|
-
* Events without a `data` field are never yielded (per the SSE spec
|
|
4
|
+
* Events without a `data` field are never yielded (per the SSE spec - only a
|
|
5
5
|
* blank line that follows at least one `data:` line dispatches an event).
|
|
6
6
|
*/
|
|
7
7
|
export interface SseEvent {
|
|
@@ -24,7 +24,7 @@ export interface SseEvent {
|
|
|
24
24
|
* CR-only line terminators are not supported (every mainstream SSE
|
|
25
25
|
* implementation emits LF or CRLF).
|
|
26
26
|
*
|
|
27
|
-
* Pure, runtime-agnostic
|
|
27
|
+
* Pure, runtime-agnostic - works anywhere `ReadableStream<Uint8Array>` and
|
|
28
28
|
* `TextDecoder` exist (browsers and Node 22+).
|
|
29
29
|
*
|
|
30
30
|
* The caller owns stream lifecycle: cancellation should be effected via an
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Format an SSE event frame. Pairs with {@link parseSseStream}.
|
|
3
3
|
*
|
|
4
|
-
* Multi-line `data` is split across multiple `data:` lines per the SSE spec
|
|
4
|
+
* Multi-line `data` is split across multiple `data:` lines per the SSE spec -
|
|
5
5
|
* each embedded `\n` becomes its own line, and the parser rejoins them.
|
|
6
6
|
*
|
|
7
7
|
* The returned string includes a trailing blank line (the dispatch marker).
|
|
@@ -13,7 +13,7 @@ interface ActorEntry {
|
|
|
13
13
|
/**
|
|
14
14
|
* In-process message bus connecting one or more {@link InMemoryTransport}
|
|
15
15
|
* instances. Routes broadcasts to every registered actor and directed messages
|
|
16
|
-
* to the actor that owns the recipient DID
|
|
16
|
+
* to the actor that owns the recipient DID, with no relay, server, or network.
|
|
17
17
|
*
|
|
18
18
|
* Each delivery does a JSON round-trip (Uint8Array preserved as `__bytes` hex)
|
|
19
19
|
* so handlers receive an isolated, serialization-faithful copy, exactly as a
|
|
@@ -64,9 +64,9 @@ interface ActorEntry {
|
|
|
64
64
|
* sending or receiving.
|
|
65
65
|
*
|
|
66
66
|
* Message routing:
|
|
67
|
-
* - Keygen messages (COHORT_ADVERT, COHORT_OPT_IN, COHORT_OPT_IN_ACCEPT, COHORT_READY)
|
|
68
|
-
* - Update messages (SUBMIT_UPDATE, DISTRIBUTE_AGGREGATED_DATA, VALIDATION_ACK)
|
|
69
|
-
* - Sign messages
|
|
67
|
+
* - Keygen messages (COHORT_ADVERT, COHORT_OPT_IN, COHORT_OPT_IN_ACCEPT, COHORT_READY) to kind 1 (plaintext)
|
|
68
|
+
* - Update messages (SUBMIT_UPDATE, DISTRIBUTE_AGGREGATED_DATA, VALIDATION_ACK) to kind 1059 (NIP-44 encrypted)
|
|
69
|
+
* - Sign messages to kind 1059 (NIP-44 encrypted)
|
|
70
70
|
*
|
|
71
71
|
* @class NostrTransport
|
|
72
72
|
* @implements {Transport}
|
|
@@ -354,13 +354,13 @@ export class NostrTransport implements Transport {
|
|
|
354
354
|
* backfill historical events to late subscribers: republishing gives late
|
|
355
355
|
* joiners a window to discover the message without requiring protocol
|
|
356
356
|
* changes. Relay rate-limit / publish failures inside the interval are
|
|
357
|
-
* caught and logged rather than propagated
|
|
357
|
+
* caught and logged rather than propagated - the caller should stop the
|
|
358
358
|
* repeater once the protocol condition is satisfied.
|
|
359
359
|
*/
|
|
360
360
|
publishRepeating(message: BaseMessage, sender: Did, intervalMs: number, recipient?: Did): () => void {
|
|
361
361
|
let stopped = false;
|
|
362
362
|
// Fire the first publish eagerly; any error surfaces as a rejected
|
|
363
|
-
// promise that we swallow to avoid unhandled rejections
|
|
363
|
+
// promise that we swallow to avoid unhandled rejections - the caller can
|
|
364
364
|
// observe delivery via receive-side handlers.
|
|
365
365
|
void this.sendMessage(message, sender, recipient).catch((err) => {
|
|
366
366
|
this.#logger.debug('publishRepeating first send failed:', err);
|
|
@@ -418,7 +418,7 @@ export class NostrTransport implements Transport {
|
|
|
418
418
|
// Relay self-echo: sendMessage() adds the sender's own pubkey to the
|
|
419
419
|
// event's `p` tags (so recipients can reply). The directed subscription
|
|
420
420
|
// filter `{'#p': [actor_pk]}` therefore matches every event this actor
|
|
421
|
-
// publishes. Skip
|
|
421
|
+
// publishes. Skip - we don't need to process our own outgoing events,
|
|
422
422
|
// and attempting to NIP-44-decrypt them fails with "invalid MAC" because
|
|
423
423
|
// the content was encrypted for the recipient, not self.
|
|
424
424
|
if(event.pubkey === bytesToHex(actor.keys.publicKey.x)) return;
|
|
@@ -14,7 +14,7 @@ export type TransportType = 'nostr' | 'didcomm' | 'http';
|
|
|
14
14
|
* shared by all registered actors. Each actor registers its own DID and keys;
|
|
15
15
|
* the transport resolves the correct identity when sending or receiving messages.
|
|
16
16
|
*
|
|
17
|
-
* The transport is a pure passthrough
|
|
17
|
+
* The transport is a pure passthrough: it knows nothing about the aggregation
|
|
18
18
|
* protocol. It only signs/encrypts outgoing messages with the sender's keys and
|
|
19
19
|
* dispatches incoming messages to the correct actor's registered handler.
|
|
20
20
|
*
|
|
@@ -23,7 +23,7 @@ export type TransportType = 'nostr' | 'didcomm' | 'http';
|
|
|
23
23
|
export interface Transport {
|
|
24
24
|
name: string;
|
|
25
25
|
|
|
26
|
-
/** Start the underlying transport (idempotent
|
|
26
|
+
/** Start the underlying transport (idempotent, only starts once). */
|
|
27
27
|
start(): void;
|
|
28
28
|
|
|
29
29
|
/** Register an actor (service or participant) with this transport. */
|
|
@@ -61,14 +61,14 @@ export interface Transport {
|
|
|
61
61
|
* message is satisfied).
|
|
62
62
|
*
|
|
63
63
|
* Useful for broadcasts on transports that don't reliably backfill
|
|
64
|
-
* historical events to late subscribers (many Nostr relays)
|
|
64
|
+
* historical events to late subscribers (many Nostr relays): republishing
|
|
65
65
|
* gives late joiners a window in which to discover the message. The first
|
|
66
66
|
* publish is synchronous-ish (fired before the method returns).
|
|
67
67
|
*
|
|
68
68
|
* Callers specify `recipient` only for directed messages; for broadcasts
|
|
69
69
|
* it is omitted.
|
|
70
70
|
*
|
|
71
|
-
* @returns A stop function. Idempotent
|
|
71
|
+
* @returns A stop function. Idempotent, safe to call more than once.
|
|
72
72
|
*/
|
|
73
73
|
publishRepeating(
|
|
74
74
|
message: BaseMessage,
|