@did-btcr2/method 0.28.0 → 0.32.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/README.md +13 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +34125 -44647
- package/dist/browser.mjs +26409 -36931
- package/dist/cjs/index.js +2869 -679
- package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
- package/dist/esm/core/aggregation/cohort.js +31 -8
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/logger.js +15 -0
- package/dist/esm/core/aggregation/logger.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js +12 -1
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +90 -0
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/index.js +1 -0
- package/dist/esm/core/aggregation/messages/index.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +39 -46
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +33 -7
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +143 -15
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +44 -5
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
- package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
- package/dist/esm/core/aggregation/transport/factory.js +15 -6
- package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/client.js +350 -0
- package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
- package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/index.js +12 -0
- package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/server.js +481 -0
- package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/index.js +1 -0
- package/dist/esm/core/aggregation/transport/index.js.map +1 -1
- package/dist/esm/core/aggregation/transport/nostr.js +245 -16
- package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
- package/dist/esm/core/beacon/beacon.js +295 -63
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +3 -3
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +3 -3
- 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/beacon/utils.js +14 -9
- package/dist/esm/core/beacon/utils.js.map +1 -1
- package/dist/esm/core/updater.js +63 -55
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +0 -4
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/did-document.js +2 -2
- package/dist/esm/utils/did-document.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
- package/dist/types/core/aggregation/cohort.d.ts +20 -3
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/logger.d.ts +22 -0
- package/dist/types/core/aggregation/logger.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/base.d.ts +13 -1
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/index.d.ts +1 -0
- package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +2 -0
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +32 -6
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +7 -5
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +33 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +5 -1
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
- package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
- package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
- package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/index.d.ts +1 -0
- package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/transport.d.ts +26 -1
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +149 -22
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/utils.d.ts +2 -2
- package/dist/types/core/beacon/utils.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts +27 -12
- package/dist/types/core/updater.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -7
- package/src/core/aggregation/beacon-strategy.ts +123 -0
- package/src/core/aggregation/cohort.ts +34 -8
- package/src/core/aggregation/logger.ts +33 -0
- package/src/core/aggregation/messages/base.ts +20 -5
- package/src/core/aggregation/messages/bodies.ts +223 -0
- package/src/core/aggregation/messages/factories.ts +1 -0
- package/src/core/aggregation/messages/index.ts +1 -0
- package/src/core/aggregation/participant.ts +40 -46
- package/src/core/aggregation/runner/events.ts +27 -3
- package/src/core/aggregation/runner/participant-runner.ts +41 -7
- package/src/core/aggregation/runner/service-runner.ts +227 -19
- package/src/core/aggregation/service.ts +189 -20
- package/src/core/aggregation/signing-session.ts +65 -7
- package/src/core/aggregation/transport/didcomm.ts +17 -0
- package/src/core/aggregation/transport/factory.ts +48 -12
- package/src/core/aggregation/transport/http/client.ts +409 -0
- package/src/core/aggregation/transport/http/envelope.ts +204 -0
- package/src/core/aggregation/transport/http/errors.ts +11 -0
- package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
- package/src/core/aggregation/transport/http/index.ts +11 -0
- package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
- package/src/core/aggregation/transport/http/protocol.ts +57 -0
- package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
- package/src/core/aggregation/transport/http/request-auth.ts +164 -0
- package/src/core/aggregation/transport/http/server.ts +615 -0
- package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
- package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
- package/src/core/aggregation/transport/index.ts +1 -0
- package/src/core/aggregation/transport/nostr.ts +266 -23
- package/src/core/aggregation/transport/transport.ts +34 -1
- package/src/core/beacon/beacon.ts +411 -79
- package/src/core/beacon/cas-beacon.ts +4 -4
- package/src/core/beacon/singleton-beacon.ts +4 -4
- package/src/core/beacon/smt-beacon.ts +4 -4
- package/src/core/beacon/utils.ts +16 -11
- package/src/core/updater.ts +113 -67
- package/src/did-btcr2.ts +0 -5
- package/src/index.ts +2 -0
- package/src/utils/did-document.ts +2 -2
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { canonicalize } from '@did-btcr2/common';
|
|
1
2
|
import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
|
|
3
|
+
import { BIP340Cryptosuite, SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
2
4
|
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
3
5
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
4
|
-
import type { Transaction } from '
|
|
6
|
+
import type { Transaction } from '@scure/btc-signer';
|
|
7
|
+
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
5
8
|
import { AggregationCohort } from './cohort.js';
|
|
6
9
|
import { AggregationServiceError } from './errors.js';
|
|
7
10
|
import type { BaseMessage } from './messages/base.js';
|
|
11
|
+
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
8
12
|
import {
|
|
9
13
|
COHORT_OPT_IN,
|
|
10
14
|
NONCE_CONTRIBUTION,
|
|
@@ -13,12 +17,12 @@ import {
|
|
|
13
17
|
VALIDATION_ACK,
|
|
14
18
|
} from './messages/constants.js';
|
|
15
19
|
import {
|
|
20
|
+
createAggregatedNonceMessage,
|
|
16
21
|
createAuthorizationRequestMessage,
|
|
17
22
|
createCohortAdvertMessage,
|
|
18
23
|
createCohortOptInAcceptMessage,
|
|
19
24
|
createCohortReadyMessage,
|
|
20
25
|
createDistributeAggregatedDataMessage,
|
|
21
|
-
createAggregatedNonceMessage,
|
|
22
26
|
} from './messages/factories.js';
|
|
23
27
|
import type { ServiceCohortPhaseType } from './phases.js';
|
|
24
28
|
import { ServiceCohortPhase } from './phases.js';
|
|
@@ -61,6 +65,23 @@ export interface SigningTxData {
|
|
|
61
65
|
prevOutValues: bigint[];
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
/** Reason an incoming message was silently dropped by the state machine. */
|
|
69
|
+
export type RejectionReason =
|
|
70
|
+
| 'WRONG_VERSION'
|
|
71
|
+
| 'UPDATE_TOO_LARGE'
|
|
72
|
+
| 'UPDATE_VERIFICATION_FAILED'
|
|
73
|
+
| 'UPDATE_MALFORMED';
|
|
74
|
+
|
|
75
|
+
/** Record of a silently-dropped inbound message. Drained by the runner to emit events. */
|
|
76
|
+
export interface Rejection {
|
|
77
|
+
/** DID of the sender whose message was rejected. */
|
|
78
|
+
from: string;
|
|
79
|
+
/** Machine-readable code. */
|
|
80
|
+
code: RejectionReason;
|
|
81
|
+
/** Human-readable reason. */
|
|
82
|
+
reason: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/** Per-cohort service state — internal. */
|
|
65
86
|
interface ServiceCohortState {
|
|
66
87
|
phase: ServiceCohortPhaseType;
|
|
@@ -70,11 +91,22 @@ interface ServiceCohortState {
|
|
|
70
91
|
acceptedParticipants: Set<string>;
|
|
71
92
|
signingSession?: BeaconSigningSession;
|
|
72
93
|
result?: AggregationResult;
|
|
94
|
+
/** Rejections accumulated since last drain. Runner polls via drainRejections(). */
|
|
95
|
+
rejections: Array<Rejection>;
|
|
73
96
|
}
|
|
74
97
|
|
|
98
|
+
/** Default maximum canonicalized byte-length of a submitted BTCR2 update. */
|
|
99
|
+
export const DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
|
|
100
|
+
|
|
75
101
|
export interface AggregationServiceParams {
|
|
76
102
|
did: string;
|
|
77
103
|
keys: SchnorrKeyPair;
|
|
104
|
+
/**
|
|
105
|
+
* Maximum canonicalized byte-length of a signed update body accepted by the
|
|
106
|
+
* service. Submissions above this cap are silently dropped and surfaced as
|
|
107
|
+
* `UPDATE_TOO_LARGE` rejections. Defaults to {@link DEFAULT_MAX_UPDATE_SIZE_BYTES}.
|
|
108
|
+
*/
|
|
109
|
+
maxUpdateSizeBytes?: number;
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
/**
|
|
@@ -91,17 +123,36 @@ export interface AggregationServiceParams {
|
|
|
91
123
|
export class AggregationService {
|
|
92
124
|
readonly did: string;
|
|
93
125
|
readonly keys: SchnorrKeyPair;
|
|
126
|
+
readonly maxUpdateSizeBytes: number;
|
|
94
127
|
|
|
95
128
|
/** Per-cohort state, keyed by cohortId. */
|
|
96
129
|
#cohortStates: Map<string, ServiceCohortState> = new Map();
|
|
97
130
|
|
|
98
|
-
constructor({ did, keys }: AggregationServiceParams) {
|
|
131
|
+
constructor({ did, keys, maxUpdateSizeBytes }: AggregationServiceParams) {
|
|
99
132
|
this.did = did;
|
|
100
133
|
this.keys = keys;
|
|
134
|
+
this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
|
|
104
138
|
receive(message: BaseMessage): void {
|
|
139
|
+
// Reject messages whose wire version doesn't match what this build speaks.
|
|
140
|
+
// Missing version → treat as legacy and drop: bumping the protocol must be
|
|
141
|
+
// coordinated across all participants.
|
|
142
|
+
const version = message.version;
|
|
143
|
+
if(version === undefined || version !== AGGREGATION_WIRE_VERSION) {
|
|
144
|
+
const cohortId = message.body?.cohortId;
|
|
145
|
+
const state = cohortId ? this.#cohortStates.get(cohortId) : undefined;
|
|
146
|
+
if(state) {
|
|
147
|
+
state.rejections.push({
|
|
148
|
+
from : message.from,
|
|
149
|
+
code : 'WRONG_VERSION',
|
|
150
|
+
reason : `Expected wire version ${AGGREGATION_WIRE_VERSION}, got ${String(version)}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
105
156
|
const type = message.type;
|
|
106
157
|
switch(type) {
|
|
107
158
|
case COHORT_OPT_IN:
|
|
@@ -125,6 +176,19 @@ export class AggregationService {
|
|
|
125
176
|
}
|
|
126
177
|
}
|
|
127
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Drain the rejection log for a cohort. Used by runners to surface silent
|
|
181
|
+
* drops (bad proof, oversized update, wrong version, etc.) as structured
|
|
182
|
+
* events without breaking the sans-I/O state machine contract.
|
|
183
|
+
*/
|
|
184
|
+
drainRejections(cohortId: string): Array<Rejection> {
|
|
185
|
+
const state = this.#cohortStates.get(cohortId);
|
|
186
|
+
if(!state) return [];
|
|
187
|
+
const out = state.rejections;
|
|
188
|
+
state.rejections = [];
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
128
192
|
|
|
129
193
|
/**
|
|
130
194
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
@@ -143,6 +207,7 @@ export class AggregationService {
|
|
|
143
207
|
config,
|
|
144
208
|
pendingOptIns : new Map(),
|
|
145
209
|
acceptedParticipants : new Set(),
|
|
210
|
+
rejections : [],
|
|
146
211
|
});
|
|
147
212
|
return cohort.id;
|
|
148
213
|
}
|
|
@@ -202,6 +267,13 @@ export class AggregationService {
|
|
|
202
267
|
const communicationPk = message.body?.communicationPk;
|
|
203
268
|
if(!participantPk || !communicationPk) return;
|
|
204
269
|
|
|
270
|
+
// Reject re-opt-in from already-accepted participants. Without this guard a
|
|
271
|
+
// participant could send a second opt-in with a different key, overwriting
|
|
272
|
+
// pendingOptIns[did] while cohortKeys still holds the original key — opening
|
|
273
|
+
// a desync window where #verifySubmittedUpdate accepts updates signed with
|
|
274
|
+
// a key that is NOT in the MuSig2 cohort.
|
|
275
|
+
if(state.acceptedParticipants.has(participantDid)) return;
|
|
276
|
+
|
|
205
277
|
state.pendingOptIns.set(participantDid, {
|
|
206
278
|
cohortId,
|
|
207
279
|
participantDid,
|
|
@@ -235,6 +307,7 @@ export class AggregationService {
|
|
|
235
307
|
|
|
236
308
|
state.acceptedParticipants.add(participantDid);
|
|
237
309
|
state.cohort.participants.push(participantDid);
|
|
310
|
+
state.cohort.participantKeys.set(participantDid, optIn.participantPk);
|
|
238
311
|
state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
|
|
239
312
|
|
|
240
313
|
return [createCohortOptInAcceptMessage({
|
|
@@ -290,6 +363,13 @@ export class AggregationService {
|
|
|
290
363
|
return state.cohort.pendingUpdates;
|
|
291
364
|
}
|
|
292
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Handle an incoming SUBMIT_UPDATE message from a participant containing their signed update to
|
|
368
|
+
* submit for aggregation.
|
|
369
|
+
* @param {BaseMessage} message - incoming SUBMIT_UPDATE message containing a participant's signed
|
|
370
|
+
* update to submit for aggregation
|
|
371
|
+
* @returns {void} - no return value; updates the service state with the submitted update if valid
|
|
372
|
+
*/
|
|
293
373
|
#handleSubmitUpdate(message: BaseMessage): void {
|
|
294
374
|
const cohortId = message.body?.cohortId;
|
|
295
375
|
if(!cohortId) return;
|
|
@@ -297,10 +377,43 @@ export class AggregationService {
|
|
|
297
377
|
if(!state) return;
|
|
298
378
|
if(state.phase !== ServiceCohortPhase.CohortSet && state.phase !== ServiceCohortPhase.CollectingUpdates) return;
|
|
299
379
|
|
|
300
|
-
const signedUpdate = message.body?.signedUpdate;
|
|
301
|
-
if(!signedUpdate)
|
|
380
|
+
const signedUpdate = message.body?.signedUpdate as SignedBTCR2Update | undefined;
|
|
381
|
+
if(!signedUpdate) {
|
|
382
|
+
state.rejections.push({
|
|
383
|
+
from : message.from,
|
|
384
|
+
code : 'UPDATE_MALFORMED',
|
|
385
|
+
reason : 'SUBMIT_UPDATE missing signedUpdate body',
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Cap the canonicalized update size before doing any heavier verification
|
|
391
|
+
// work. Without this guard, a participant could submit multi-MB payloads
|
|
392
|
+
// that the service would canonicalize, hash, and aggregate — cheap DoS.
|
|
393
|
+
const canonicalSize = canonicalize(signedUpdate as unknown as Record<string, unknown>).length;
|
|
394
|
+
if(canonicalSize > this.maxUpdateSizeBytes) {
|
|
395
|
+
state.rejections.push({
|
|
396
|
+
from : message.from,
|
|
397
|
+
code : 'UPDATE_TOO_LARGE',
|
|
398
|
+
reason : `Canonicalized update is ${canonicalSize} bytes; max allowed is ${this.maxUpdateSizeBytes}`,
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Verify the BIP-340 Data Integrity proof before aggregating. Without this check,
|
|
404
|
+
// a malicious cohort member could submit updates with garbage proofs, which the
|
|
405
|
+
// service would aggregate into the CAS announcement / SMT root and ultimately
|
|
406
|
+
// anchor on-chain with the cohort's MuSig2 signature.
|
|
407
|
+
if(!this.#verifySubmittedUpdate(state, message.from, signedUpdate)) {
|
|
408
|
+
state.rejections.push({
|
|
409
|
+
from : message.from,
|
|
410
|
+
code : 'UPDATE_VERIFICATION_FAILED',
|
|
411
|
+
reason : 'BIP-340 Data Integrity proof verification failed',
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
302
415
|
|
|
303
|
-
state.cohort.addUpdate(message.from, signedUpdate
|
|
416
|
+
state.cohort.addUpdate(message.from, signedUpdate);
|
|
304
417
|
|
|
305
418
|
if(state.phase === ServiceCohortPhase.CohortSet) {
|
|
306
419
|
state.phase = ServiceCohortPhase.CollectingUpdates;
|
|
@@ -310,6 +423,46 @@ export class AggregationService {
|
|
|
310
423
|
}
|
|
311
424
|
}
|
|
312
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Verify the BIP-340 Schnorr Data Integrity proof on a submitted update using the
|
|
428
|
+
* participant's public key from their cohort opt-in. Returns `false` (and the
|
|
429
|
+
* update is silently dropped) if the proof is missing, the verificationMethod does
|
|
430
|
+
* not name the sender's DID, the participant has no opt-in on record, or the
|
|
431
|
+
* signature fails verification.
|
|
432
|
+
* @param {ServiceCohortState} state - the current state of the cohort to which the update was submitted
|
|
433
|
+
* @param {string} sender - the DID of the participant who submitted the update
|
|
434
|
+
* @param {SignedBTCR2Update} signedUpdate - the signed update containing the proof to verify
|
|
435
|
+
* @returns {boolean} - `true` if the proof is valid and the update can be accepted; `false` otherwise
|
|
436
|
+
*/
|
|
437
|
+
#verifySubmittedUpdate(
|
|
438
|
+
state: ServiceCohortState,
|
|
439
|
+
sender: string,
|
|
440
|
+
signedUpdate: SignedBTCR2Update,
|
|
441
|
+
): boolean {
|
|
442
|
+
const proof = signedUpdate.proof;
|
|
443
|
+
if(!proof?.verificationMethod || !proof.proofValue) return false;
|
|
444
|
+
|
|
445
|
+
// The proof must be signed by the sender's own key. Reject if the
|
|
446
|
+
// verificationMethod references a different DID.
|
|
447
|
+
const vmDid = proof.verificationMethod.split('#')[0];
|
|
448
|
+
if(vmDid !== sender) return false;
|
|
449
|
+
|
|
450
|
+
const optIn = state.pendingOptIns.get(sender);
|
|
451
|
+
if(!optIn) return false;
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const multikey = SchnorrMultikey.fromPublicKey({
|
|
455
|
+
id : proof.verificationMethod,
|
|
456
|
+
controller : sender,
|
|
457
|
+
publicKeyBytes : optIn.participantPk,
|
|
458
|
+
}) as SchnorrMultikey;
|
|
459
|
+
const suite = new BIP340Cryptosuite(multikey);
|
|
460
|
+
return suite.verifyProof(signedUpdate).verified === true;
|
|
461
|
+
} catch {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
313
466
|
|
|
314
467
|
/**
|
|
315
468
|
* Build the aggregated data structure (CAS Announcement or SMT tree) and
|
|
@@ -327,30 +480,29 @@ export class AggregationService {
|
|
|
327
480
|
);
|
|
328
481
|
}
|
|
329
482
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
} else if(state.config.beaconType === 'SMTBeacon') {
|
|
333
|
-
state.cohort.buildSMTTree();
|
|
334
|
-
} else {
|
|
483
|
+
const strategy = getBeaconStrategy(state.config.beaconType);
|
|
484
|
+
if(!strategy) {
|
|
335
485
|
throw new AggregationServiceError(
|
|
336
486
|
`Unsupported beacon type: ${state.config.beaconType}`,
|
|
337
487
|
'UNSUPPORTED_BEACON_TYPE', { cohortId, beaconType: state.config.beaconType }
|
|
338
488
|
);
|
|
339
489
|
}
|
|
490
|
+
strategy.buildAggregatedData(state.cohort);
|
|
340
491
|
|
|
341
492
|
const signalBytesHex = bytesToHex(state.cohort.signalBytes!);
|
|
342
493
|
state.phase = ServiceCohortPhase.DataDistributed;
|
|
343
494
|
|
|
344
495
|
const messages: BaseMessage[] = [];
|
|
345
496
|
for(const participantDid of state.cohort.participants) {
|
|
497
|
+
const payload = strategy.getDistributePayload(state.cohort, participantDid);
|
|
346
498
|
messages.push(createDistributeAggregatedDataMessage({
|
|
347
499
|
from : this.did,
|
|
348
500
|
to : participantDid,
|
|
349
501
|
cohortId,
|
|
350
502
|
beaconType : state.config.beaconType,
|
|
351
503
|
signalBytesHex,
|
|
352
|
-
casAnnouncement :
|
|
353
|
-
smtProof :
|
|
504
|
+
casAnnouncement : payload.casAnnouncement,
|
|
505
|
+
smtProof : payload.smtProof,
|
|
354
506
|
}));
|
|
355
507
|
}
|
|
356
508
|
return messages;
|
|
@@ -422,15 +574,24 @@ export class AggregationService {
|
|
|
422
574
|
state.signingSession = session;
|
|
423
575
|
state.phase = ServiceCohortPhase.SigningStarted;
|
|
424
576
|
|
|
577
|
+
const prevOutScript = txData.prevOutScripts[0];
|
|
578
|
+
if(!prevOutScript) {
|
|
579
|
+
throw new AggregationServiceError(
|
|
580
|
+
`Cannot start signing for cohort ${cohortId}: txData.prevOutScripts[0] is missing.`,
|
|
581
|
+
'MISSING_PREV_OUT_SCRIPT', { cohortId }
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
425
585
|
const messages: BaseMessage[] = [];
|
|
426
586
|
for(const participantDid of state.cohort.participants) {
|
|
427
587
|
messages.push(createAuthorizationRequestMessage({
|
|
428
|
-
from
|
|
429
|
-
to
|
|
588
|
+
from : this.did,
|
|
589
|
+
to : participantDid,
|
|
430
590
|
cohortId,
|
|
431
|
-
sessionId
|
|
432
|
-
pendingTx
|
|
433
|
-
|
|
591
|
+
sessionId : session.id,
|
|
592
|
+
pendingTx : txData.tx.hex,
|
|
593
|
+
prevOutScriptHex : bytesToHex(prevOutScript),
|
|
594
|
+
prevOutValue : txData.prevOutValues[0]?.toString() ?? '0',
|
|
434
595
|
}));
|
|
435
596
|
}
|
|
436
597
|
return messages;
|
|
@@ -507,8 +668,8 @@ export class AggregationService {
|
|
|
507
668
|
// All partial sigs received — generate final signature
|
|
508
669
|
const signature = state.signingSession.generateFinalSignature();
|
|
509
670
|
|
|
510
|
-
// Set Taproot key-path witness
|
|
511
|
-
state.signingSession.pendingTx.
|
|
671
|
+
// Set Taproot key-path witness (finalScriptWitness injects the aggregated MuSig2 sig)
|
|
672
|
+
state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
|
|
512
673
|
|
|
513
674
|
state.result = {
|
|
514
675
|
cohortId,
|
|
@@ -544,4 +705,12 @@ export class AggregationService {
|
|
|
544
705
|
get cohorts(): ReadonlyArray<AggregationCohort> {
|
|
545
706
|
return [...this.#cohortStates.values()].map(s => s.cohort);
|
|
546
707
|
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Remove a cohort from the state map. Used by runners to GC state on cohort
|
|
711
|
+
* completion, failure, or expiry. No-op if the cohort doesn't exist.
|
|
712
|
+
*/
|
|
713
|
+
removeCohort(cohortId: string): void {
|
|
714
|
+
this.#cohortStates.delete(cohortId);
|
|
715
|
+
}
|
|
547
716
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { Transaction } from '@scure/btc-signer';
|
|
2
|
+
import { SigHash } from '@scure/btc-signer';
|
|
1
3
|
import * as musig2 from '@scure/btc-signer/musig2';
|
|
2
|
-
import { Transaction } from 'bitcoinjs-lib';
|
|
3
4
|
import type { AggregationCohort } from './cohort.js';
|
|
4
5
|
import { SigningSessionError } from './errors.js';
|
|
5
6
|
import type { SigningSessionPhaseType } from './phases.js';
|
|
@@ -79,11 +80,11 @@ export class BeaconSigningSession {
|
|
|
79
80
|
'SIGHASH_ERROR'
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
|
-
return this.pendingTx.
|
|
83
|
+
return this.pendingTx.preimageWitnessV1(
|
|
83
84
|
0,
|
|
84
85
|
this.prevOutScripts,
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
SigHash.DEFAULT,
|
|
87
|
+
this.prevOutValues
|
|
87
88
|
);
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -94,6 +95,12 @@ export class BeaconSigningSession {
|
|
|
94
95
|
'INVALID_PHASE', { phase: this.phase }
|
|
95
96
|
);
|
|
96
97
|
}
|
|
98
|
+
if(!this.cohort.participants.includes(participantDid)) {
|
|
99
|
+
throw new SigningSessionError(
|
|
100
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
101
|
+
'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
97
104
|
if(nonceContribution.length !== 66) {
|
|
98
105
|
throw new SigningSessionError(
|
|
99
106
|
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
@@ -132,6 +139,12 @@ export class BeaconSigningSession {
|
|
|
132
139
|
'INVALID_PHASE'
|
|
133
140
|
);
|
|
134
141
|
}
|
|
142
|
+
if(!this.cohort.participants.includes(participantDid)) {
|
|
143
|
+
throw new SigningSessionError(
|
|
144
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
145
|
+
'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
135
148
|
if(this.partialSignatures.has(participantDid)) {
|
|
136
149
|
throw new SigningSessionError(
|
|
137
150
|
`Duplicate partial signature from ${participantDid}.`,
|
|
@@ -159,9 +172,47 @@ export class BeaconSigningSession {
|
|
|
159
172
|
this.aggregatedNonce,
|
|
160
173
|
this.cohort.cohortKeys,
|
|
161
174
|
this.sigHash,
|
|
162
|
-
[this.cohort.
|
|
175
|
+
[this.cohort.tapTweak],
|
|
163
176
|
[true]
|
|
164
177
|
);
|
|
178
|
+
|
|
179
|
+
// Pre-verify each partial signature against the signer's public key before
|
|
180
|
+
// aggregating (BIP-327 §2.3.5). Delegating verification to partialSigAgg
|
|
181
|
+
// alone makes it impossible to attribute a bad contribution; pinpointing
|
|
182
|
+
// the offending participant lets the service blame and retry without the
|
|
183
|
+
// whole cohort.
|
|
184
|
+
//
|
|
185
|
+
// partialSigVerify(partialSig, pubNonces, i) needs pubNonces ordered to
|
|
186
|
+
// match cohort.cohortKeys, and `i` is the signer's position in that array.
|
|
187
|
+
const pubNoncesByIndex: Uint8Array[] = new Array(this.cohort.cohortKeys.length);
|
|
188
|
+
for(const [did, nonce] of this.nonceContributions) {
|
|
189
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
190
|
+
if(idx < 0) {
|
|
191
|
+
throw new SigningSessionError(
|
|
192
|
+
`Cannot verify nonce from ${did}: participant key missing from cohort.`,
|
|
193
|
+
'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
pubNoncesByIndex[idx] = nonce;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for(const [did, partialSig] of this.partialSignatures) {
|
|
200
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
201
|
+
if(idx < 0) {
|
|
202
|
+
throw new SigningSessionError(
|
|
203
|
+
`Cannot verify partial signature from ${did}: participant key missing from cohort.`,
|
|
204
|
+
'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
|
|
208
|
+
if(!ok) {
|
|
209
|
+
throw new SigningSessionError(
|
|
210
|
+
`Bad partial signature from ${did}.`,
|
|
211
|
+
'BAD_PARTIAL_SIG', { participantDid: did, index: idx }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
165
216
|
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
166
217
|
this.phase = SigningSessionPhase.Complete;
|
|
167
218
|
return this.signature;
|
|
@@ -181,6 +232,10 @@ export class BeaconSigningSession {
|
|
|
181
232
|
/**
|
|
182
233
|
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
183
234
|
* Requires the aggregated nonce to have been set first (via the service).
|
|
235
|
+
*
|
|
236
|
+
* Zeros the stored `secretNonce` after use. JS cannot truly erase memory (GC
|
|
237
|
+
* and immutable strings), but overwriting the bytes shortens the exposure
|
|
238
|
+
* window and prevents accidental reuse or serialization of a spent nonce.
|
|
184
239
|
*/
|
|
185
240
|
public generatePartialSignature(participantSecretKey: Uint8Array): Uint8Array {
|
|
186
241
|
if(!this.aggregatedNonce) {
|
|
@@ -193,10 +248,13 @@ export class BeaconSigningSession {
|
|
|
193
248
|
this.aggregatedNonce,
|
|
194
249
|
this.cohort.cohortKeys,
|
|
195
250
|
this.sigHash,
|
|
196
|
-
[this.cohort.
|
|
251
|
+
[this.cohort.tapTweak],
|
|
197
252
|
[true]
|
|
198
253
|
);
|
|
199
|
-
|
|
254
|
+
const partialSig = session.sign(this.secretNonce, participantSecretKey);
|
|
255
|
+
this.secretNonce.fill(0);
|
|
256
|
+
this.secretNonce = undefined;
|
|
257
|
+
return partialSig;
|
|
200
258
|
}
|
|
201
259
|
|
|
202
260
|
public isComplete(): boolean {
|
|
@@ -36,7 +36,24 @@ export class DidCommTransport implements Transport {
|
|
|
36
36
|
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
public unregisterMessageHandler(_actorDid: string, _messageType: string): void {
|
|
40
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public unregisterActor(_did: string): void {
|
|
44
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
public async sendMessage(_message: BaseMessage, _sender: string, _recipient?: string): Promise<void> {
|
|
40
48
|
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
41
49
|
}
|
|
50
|
+
|
|
51
|
+
public publishRepeating(
|
|
52
|
+
_message: BaseMessage,
|
|
53
|
+
_sender: string,
|
|
54
|
+
_intervalMs: number,
|
|
55
|
+
_recipient?: string,
|
|
56
|
+
): () => void {
|
|
57
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
58
|
+
}
|
|
42
59
|
}
|
|
@@ -1,28 +1,64 @@
|
|
|
1
1
|
import { NotImplementedError } from '@did-btcr2/common';
|
|
2
|
-
import {
|
|
2
|
+
import type { Logger } from '../logger.js';
|
|
3
3
|
import { TransportError } from './error.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { HttpClientTransportConfig } from './http/client.js';
|
|
5
|
+
import { HttpClientTransport } from './http/client.js';
|
|
6
|
+
import type { HttpServerTransportConfig } from './http/server.js';
|
|
7
|
+
import { HttpServerTransport } from './http/server.js';
|
|
8
|
+
import { NostrTransport } from './nostr.js';
|
|
9
|
+
import type { Transport } from './transport.js';
|
|
10
|
+
|
|
11
|
+
/** Discriminated-union config for {@link TransportFactory.establish}. */
|
|
12
|
+
export type TransportConfig =
|
|
13
|
+
| NostrTransportConfigOption
|
|
14
|
+
| DidCommTransportConfigOption
|
|
15
|
+
| HttpClientTransportConfigOption
|
|
16
|
+
| HttpServerTransportConfigOption;
|
|
5
17
|
|
|
6
|
-
export interface
|
|
7
|
-
type:
|
|
18
|
+
export interface NostrTransportConfigOption {
|
|
19
|
+
type: 'nostr';
|
|
8
20
|
relays?: string[];
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
broadcastLookbackMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DidCommTransportConfigOption {
|
|
26
|
+
type: 'didcomm';
|
|
9
27
|
}
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
export interface HttpClientTransportConfigOption extends HttpClientTransportConfig {
|
|
30
|
+
type: 'http';
|
|
31
|
+
role: 'client';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HttpServerTransportConfigOption extends HttpServerTransportConfig {
|
|
35
|
+
type: 'http';
|
|
36
|
+
role: 'server';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Factory for creating Transport instances. */
|
|
15
40
|
export class TransportFactory {
|
|
16
41
|
static establish(config: TransportConfig): Transport {
|
|
17
|
-
switch
|
|
42
|
+
switch(config.type) {
|
|
18
43
|
case 'nostr':
|
|
19
|
-
return new NostrTransport({
|
|
44
|
+
return new NostrTransport({
|
|
45
|
+
relays : config.relays,
|
|
46
|
+
logger : config.logger,
|
|
47
|
+
broadcastLookbackMs : config.broadcastLookbackMs,
|
|
48
|
+
});
|
|
20
49
|
case 'didcomm':
|
|
21
50
|
throw new NotImplementedError('DIDComm transport not implemented yet.');
|
|
51
|
+
case 'http':
|
|
52
|
+
if(config.role === 'client') return new HttpClientTransport(config);
|
|
53
|
+
if(config.role === 'server') return new HttpServerTransport(config);
|
|
54
|
+
throw new NotImplementedError(
|
|
55
|
+
`HTTP transport role not implemented: ${(config as { role: string }).role}`,
|
|
56
|
+
);
|
|
22
57
|
default:
|
|
23
58
|
throw new TransportError(
|
|
24
|
-
`Invalid transport type: ${config.type}`,
|
|
25
|
-
'INVALID_TRANSPORT_TYPE',
|
|
59
|
+
`Invalid transport type: ${(config as { type: string }).type}`,
|
|
60
|
+
'INVALID_TRANSPORT_TYPE',
|
|
61
|
+
{ config },
|
|
26
62
|
);
|
|
27
63
|
}
|
|
28
64
|
}
|