@did-btcr2/method 0.36.1 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +1060 -96
- package/dist/browser.mjs +1060 -96
- package/dist/cjs/index.js +1153 -179
- package/dist/esm/core/aggregation/beacon-strategy.js +19 -6
- package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -1
- package/dist/esm/core/aggregation/cohort.js +142 -33
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/conditions.js +27 -0
- package/dist/esm/core/aggregation/conditions.js.map +1 -1
- package/dist/esm/core/aggregation/fallback-spend.js +132 -0
- package/dist/esm/core/aggregation/fallback-spend.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +46 -1
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
- package/dist/esm/core/aggregation/messages/constants.js +11 -0
- package/dist/esm/core/aggregation/messages/constants.js.map +1 -1
- package/dist/esm/core/aggregation/messages/factories.js +27 -1
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/messages/guards.js +4 -1
- package/dist/esm/core/aggregation/messages/guards.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +210 -21
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/phases.js +6 -0
- package/dist/esm/core/aggregation/phases.js.map +1 -1
- package/dist/esm/core/aggregation/recovery-policy.js +156 -0
- package/dist/esm/core/aggregation/recovery-policy.js.map +1 -0
- package/dist/esm/core/aggregation/recovery-spend.js +106 -0
- package/dist/esm/core/aggregation/recovery-spend.js.map +1 -0
- package/dist/esm/core/aggregation/runner/aggregation-runner.js +11 -3
- package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +74 -22
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +97 -9
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +221 -11
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/core/aggregation/signing-session.js +3 -3
- package/dist/esm/core/aggregation/signing-session.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/envelope.js +3 -3
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +1 -1
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +1 -1
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +1 -1
- package/dist/esm/core/aggregation/transport/http/request-auth.js +1 -1
- package/dist/esm/core/aggregation/transport/http/server.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +1 -1
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +1 -1
- package/dist/esm/core/aggregation/transport/in-memory.js +1 -1
- package/dist/esm/core/aggregation/transport/nostr.js +6 -6
- package/dist/esm/core/beacon/beacon.js +15 -10
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +6 -6
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +2 -2
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +3 -3
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/resolver.js +8 -8
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +6 -6
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +2 -2
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/core/aggregation/beacon-strategy.d.ts +8 -4
- package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -1
- package/dist/types/core/aggregation/cohort.d.ts +88 -17
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/conditions.d.ts +28 -0
- package/dist/types/core/aggregation/conditions.d.ts.map +1 -1
- package/dist/types/core/aggregation/fallback-spend.d.ts +94 -0
- package/dist/types/core/aggregation/fallback-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/logger.d.ts +1 -1
- package/dist/types/core/aggregation/messages/base.d.ts +6 -0
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +50 -2
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/constants.d.ts +8 -0
- package/dist/types/core/aggregation/messages/constants.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/factories.d.ts +40 -0
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/guards.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +41 -7
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/phases.d.ts +8 -2
- package/dist/types/core/aggregation/phases.d.ts.map +1 -1
- package/dist/types/core/aggregation/recovery-policy.d.ts +132 -0
- package/dist/types/core/aggregation/recovery-policy.d.ts.map +1 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts +94 -0
- package/dist/types/core/aggregation/recovery-spend.d.ts.map +1 -0
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +13 -3
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +28 -7
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +3 -3
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +28 -3
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +26 -2
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/core/aggregation/signing-session.d.ts +2 -2
- package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +3 -3
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/server.d.ts +1 -1
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +2 -2
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +1 -1
- package/dist/types/core/aggregation/transport/in-memory.d.ts +1 -1
- package/dist/types/core/aggregation/transport/nostr.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts +4 -4
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +7 -7
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +2 -2
- package/dist/types/core/beacon/interfaces.d.ts +1 -1
- package/dist/types/core/resolver.d.ts +3 -3
- package/dist/types/core/updater.d.ts +4 -4
- package/dist/types/did-btcr2.d.ts +2 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/beacon-strategy.ts +27 -9
- package/src/core/aggregation/cohort.ts +171 -35
- package/src/core/aggregation/conditions.ts +56 -0
- package/src/core/aggregation/fallback-spend.ts +223 -0
- package/src/core/aggregation/logger.ts +1 -1
- package/src/core/aggregation/messages/base.ts +6 -0
- package/src/core/aggregation/messages/bodies.ts +94 -1
- package/src/core/aggregation/messages/constants.ts +12 -0
- package/src/core/aggregation/messages/factories.ts +57 -0
- package/src/core/aggregation/messages/guards.ts +6 -0
- package/src/core/aggregation/participant.ts +255 -23
- package/src/core/aggregation/phases.ts +9 -0
- package/src/core/aggregation/recovery-policy.ts +237 -0
- package/src/core/aggregation/recovery-spend.ts +192 -0
- package/src/core/aggregation/runner/aggregation-runner.ts +27 -5
- package/src/core/aggregation/runner/events.ts +26 -7
- package/src/core/aggregation/runner/participant-runner.ts +75 -23
- package/src/core/aggregation/runner/service-runner.ts +120 -10
- package/src/core/aggregation/service.ts +260 -14
- package/src/core/aggregation/signing-session.ts +3 -3
- package/src/core/aggregation/transport/http/envelope.ts +3 -3
- package/src/core/aggregation/transport/http/inbox-buffer.ts +1 -1
- package/src/core/aggregation/transport/http/nonce-cache.ts +1 -1
- package/src/core/aggregation/transport/http/rate-limiter.ts +1 -1
- package/src/core/aggregation/transport/http/request-auth.ts +1 -1
- package/src/core/aggregation/transport/http/server.ts +1 -1
- package/src/core/aggregation/transport/http/sse-stream.ts +2 -2
- package/src/core/aggregation/transport/http/sse-writer.ts +1 -1
- package/src/core/aggregation/transport/in-memory.ts +1 -1
- package/src/core/aggregation/transport/nostr.ts +6 -6
- package/src/core/aggregation/transport/transport.ts +4 -4
- package/src/core/beacon/beacon.ts +16 -11
- package/src/core/beacon/cas-beacon.ts +7 -7
- package/src/core/beacon/interfaces.ts +1 -1
- package/src/core/beacon/singleton-beacon.ts +2 -2
- package/src/core/beacon/smt-beacon.ts +3 -3
- package/src/core/resolver.ts +8 -8
- package/src/core/updater.ts +6 -6
- package/src/did-btcr2.ts +2 -2
- package/src/index.ts +3 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { canonicalHash } from '@did-btcr2/common';
|
|
2
2
|
import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
|
|
3
3
|
import type { SerializedSMTProof} from '@did-btcr2/smt';
|
|
4
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
4
5
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
5
|
-
import { Transaction } from '@scure/btc-signer';
|
|
6
|
+
import { Script, Transaction } from '@scure/btc-signer';
|
|
6
7
|
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
7
8
|
import { AggregationCohort } from './cohort.js';
|
|
8
9
|
import type { CohortConditions } from './conditions.js';
|
|
9
10
|
import { AggregationParticipantError } from './errors.js';
|
|
11
|
+
import { fallbackSighash } from './fallback-spend.js';
|
|
12
|
+
import { buildFallbackLeaf } from './recovery-policy.js';
|
|
10
13
|
import type { BaseMessage } from './messages/base.js';
|
|
11
14
|
import { isCohortAdvertMessage } from './messages/bodies.js';
|
|
12
15
|
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
@@ -17,11 +20,14 @@ import {
|
|
|
17
20
|
COHORT_OPT_IN_ACCEPT,
|
|
18
21
|
COHORT_READY,
|
|
19
22
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
23
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
20
24
|
} from './messages/constants.js';
|
|
21
25
|
import {
|
|
22
26
|
createCohortOptInMessage,
|
|
27
|
+
createFallbackSignatureMessage,
|
|
23
28
|
createNonceContributionMessage,
|
|
24
29
|
createSignatureAuthorizationMessage,
|
|
30
|
+
createSubmitNonIncludedMessage,
|
|
25
31
|
createSubmitUpdateMessage,
|
|
26
32
|
createValidationAckMessage,
|
|
27
33
|
} from './messages/factories.js';
|
|
@@ -30,6 +36,30 @@ import { ParticipantCohortPhase } from './phases.js';
|
|
|
30
36
|
import type { AggregationSigner } from './signer.js';
|
|
31
37
|
import { BeaconSigningSession } from './signing-session.js';
|
|
32
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True if `tx` has an OP_RETURN output whose payload equals the 32-byte signal
|
|
41
|
+
* `signalHex`. A member binds its fallback signature to the exact signal it
|
|
42
|
+
* validated, so a coordinator that drives the fallback output selection cannot
|
|
43
|
+
* anchor a different announcement (a stale signal, or one whose CAS/SMT root
|
|
44
|
+
* omits the member's update) under the member's signature.
|
|
45
|
+
*/
|
|
46
|
+
function txEmbedsSignal(tx: Transaction, signalHex: string): boolean {
|
|
47
|
+
let expected: Uint8Array;
|
|
48
|
+
try { expected = hexToBytes(signalHex); } catch { return false; }
|
|
49
|
+
if(expected.length === 0) return false;
|
|
50
|
+
for(let i = 0; i < tx.outputsLength; i++) {
|
|
51
|
+
const script = tx.getOutput(i)?.script;
|
|
52
|
+
if(!script) continue;
|
|
53
|
+
let decoded: Array<string | Uint8Array>;
|
|
54
|
+
try { decoded = Script.decode(script) as Array<string | Uint8Array>; } catch { continue; }
|
|
55
|
+
if(decoded.length === 2 && decoded[0] === 'RETURN' && decoded[1] instanceof Uint8Array) {
|
|
56
|
+
const payload = decoded[1];
|
|
57
|
+
if(payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/**
|
|
34
64
|
* Cohort advert as discovered by the participant (UI: list of joinable cohorts).
|
|
35
65
|
* Carries the advertised {@link CohortConditions} (beaconType, minParticipants,
|
|
@@ -42,7 +72,7 @@ export interface CohortAdvert extends CohortConditions {
|
|
|
42
72
|
serviceCommunicationPk: Uint8Array;
|
|
43
73
|
}
|
|
44
74
|
|
|
45
|
-
/** Joined cohort info
|
|
75
|
+
/** Joined cohort info, available after the cohort is finalized. */
|
|
46
76
|
export interface JoinedCohortInfo {
|
|
47
77
|
cohortId: string;
|
|
48
78
|
serviceDid: string;
|
|
@@ -57,8 +87,11 @@ export interface PendingValidation {
|
|
|
57
87
|
signalBytesHex: string;
|
|
58
88
|
casAnnouncement?: Record<string, string>;
|
|
59
89
|
smtProof?: SerializedSMTProof;
|
|
90
|
+
/** Canonical hash of this participant's update; empty for a decliner. */
|
|
60
91
|
expectedHash: string;
|
|
61
92
|
matches: boolean;
|
|
93
|
+
/** True if this participant submitted an update; false if it declined (non-inclusion). */
|
|
94
|
+
included: boolean;
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
/** Pending signing request (UI: review tx for approval). */
|
|
@@ -71,7 +104,22 @@ export interface PendingSigningRequest {
|
|
|
71
104
|
prevOutValue: string;
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Pending fallback signing request (UI: review the fallback spend for approval).
|
|
109
|
+
* The service fell back to the k-of-n script path; the member signs the SAME
|
|
110
|
+
* beacon transaction over the fallback script-path sighash (ADR 042).
|
|
111
|
+
*/
|
|
112
|
+
export interface PendingFallbackRequest {
|
|
113
|
+
cohortId: string;
|
|
114
|
+
sessionId: string;
|
|
115
|
+
pendingTxHex: string;
|
|
116
|
+
prevOutScriptHex: string;
|
|
117
|
+
prevOutValue: string;
|
|
118
|
+
/** Fallback leaf script, hex (advisory; the member recomputes it from its own cohort). */
|
|
119
|
+
fallbackLeafScriptHex: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Per-cohort participant state (internal). */
|
|
75
123
|
interface ParticipantCohortState {
|
|
76
124
|
phase: ParticipantCohortPhaseType;
|
|
77
125
|
cohortId: string;
|
|
@@ -79,8 +127,15 @@ interface ParticipantCohortState {
|
|
|
79
127
|
advert?: CohortAdvert;
|
|
80
128
|
cohort?: AggregationCohort;
|
|
81
129
|
submittedUpdate?: SignedBTCR2Update;
|
|
130
|
+
/**
|
|
131
|
+
* This round's intent, persisted because the phase advances past
|
|
132
|
+
* NonIncluded/UpdateSubmitted into validation/signing. true = submitted an
|
|
133
|
+
* update, false = declined (non-inclusion), undefined = not yet responded.
|
|
134
|
+
*/
|
|
135
|
+
included?: boolean;
|
|
82
136
|
validation?: PendingValidation;
|
|
83
137
|
signingRequest?: PendingSigningRequest;
|
|
138
|
+
fallbackRequest?: PendingFallbackRequest;
|
|
84
139
|
signingSession?: BeaconSigningSession;
|
|
85
140
|
}
|
|
86
141
|
|
|
@@ -126,7 +181,7 @@ export class AggregationParticipant {
|
|
|
126
181
|
|
|
127
182
|
/**
|
|
128
183
|
* Process an incoming message. Updates internal state but never produces
|
|
129
|
-
* outgoing messages
|
|
184
|
+
* outgoing messages: those come exclusively from action methods.
|
|
130
185
|
*/
|
|
131
186
|
public receive(message: BaseMessage): void {
|
|
132
187
|
// Reject messages whose wire version doesn't match what this build speaks.
|
|
@@ -153,8 +208,11 @@ export class AggregationParticipant {
|
|
|
153
208
|
case AGGREGATED_NONCE:
|
|
154
209
|
this.#handleAggregatedNonce(message);
|
|
155
210
|
break;
|
|
211
|
+
case FALLBACK_AUTHORIZATION_REQUEST:
|
|
212
|
+
this.#handleFallbackAuthorizationRequest(message);
|
|
213
|
+
break;
|
|
156
214
|
default:
|
|
157
|
-
// Unknown message type
|
|
215
|
+
// Unknown message type, silently ignore
|
|
158
216
|
break;
|
|
159
217
|
}
|
|
160
218
|
}
|
|
@@ -207,13 +265,20 @@ export class AggregationParticipant {
|
|
|
207
265
|
);
|
|
208
266
|
}
|
|
209
267
|
|
|
210
|
-
// Create local cohort to track our view
|
|
268
|
+
// Create local cohort to track our view. Carry the advertised recovery
|
|
269
|
+
// params so validateMembership recomputes the same script-tree beacon
|
|
270
|
+
// address the service derived (ADR 042); a mismatch rejects the cohort.
|
|
271
|
+
const advert = state.advert!;
|
|
211
272
|
const cohort = new AggregationCohort({
|
|
212
|
-
id
|
|
213
|
-
serviceDid
|
|
214
|
-
minParticipants
|
|
215
|
-
network
|
|
216
|
-
beaconType
|
|
273
|
+
id : cohortId,
|
|
274
|
+
serviceDid : state.serviceDid,
|
|
275
|
+
minParticipants : advert.minParticipants,
|
|
276
|
+
network : advert.network,
|
|
277
|
+
beaconType : advert.beaconType,
|
|
278
|
+
recoveryKey : advert.recoveryKey ? hexToBytes(advert.recoveryKey) : undefined,
|
|
279
|
+
recoverySequence : advert.recoverySequence,
|
|
280
|
+
fundingModel : advert.fundingModel,
|
|
281
|
+
fallbackThreshold : advert.fallbackThreshold,
|
|
217
282
|
});
|
|
218
283
|
state.cohort = cohort;
|
|
219
284
|
state.phase = ParticipantCohortPhase.OptedIn;
|
|
@@ -230,12 +295,12 @@ export class AggregationParticipant {
|
|
|
230
295
|
}
|
|
231
296
|
|
|
232
297
|
#handleOptInAccept(message: BaseMessage): void {
|
|
233
|
-
// Acknowledgment from service
|
|
298
|
+
// Acknowledgment from service, no state change needed
|
|
234
299
|
void message;
|
|
235
300
|
}
|
|
236
301
|
|
|
237
302
|
|
|
238
|
-
/** Cohorts that have been finalized
|
|
303
|
+
/** Cohorts that have been finalized: beacon address available. */
|
|
239
304
|
public get joinedCohorts(): ReadonlyMap<string, JoinedCohortInfo> {
|
|
240
305
|
const map = new Map<string, JoinedCohortInfo>();
|
|
241
306
|
for(const [id, state] of this.#cohortStates) {
|
|
@@ -284,6 +349,7 @@ export class AggregationParticipant {
|
|
|
284
349
|
}
|
|
285
350
|
|
|
286
351
|
state.submittedUpdate = signedUpdate;
|
|
352
|
+
state.included = true;
|
|
287
353
|
state.phase = ParticipantCohortPhase.UpdateSubmitted;
|
|
288
354
|
|
|
289
355
|
const message = createSubmitUpdateMessage({
|
|
@@ -295,6 +361,34 @@ export class AggregationParticipant {
|
|
|
295
361
|
return [message];
|
|
296
362
|
}
|
|
297
363
|
|
|
364
|
+
/**
|
|
365
|
+
* User action: decline to submit an update this round (cooperative
|
|
366
|
+
* non-inclusion). The member stays in the cohort and still signs; it will be
|
|
367
|
+
* absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
|
|
368
|
+
* SMT. Returns the SUBMIT_NONINCLUDED message to send.
|
|
369
|
+
*/
|
|
370
|
+
public declineUpdate(cohortId: string): BaseMessage[] {
|
|
371
|
+
const state = this.#cohortStates.get(cohortId);
|
|
372
|
+
if(!state || state.phase !== ParticipantCohortPhase.CohortReady) {
|
|
373
|
+
throw new AggregationParticipantError(
|
|
374
|
+
`Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
|
|
375
|
+
'INVALID_PHASE', { cohortId, phase: state?.phase }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Leave submittedUpdate unset; included=false is the load-bearing signal that
|
|
380
|
+
// the validation handler uses to validate the non-inclusion slot.
|
|
381
|
+
state.included = false;
|
|
382
|
+
state.phase = ParticipantCohortPhase.NonIncluded;
|
|
383
|
+
|
|
384
|
+
const message = createSubmitNonIncludedMessage({
|
|
385
|
+
from : this.did,
|
|
386
|
+
to : state.serviceDid,
|
|
387
|
+
cohortId,
|
|
388
|
+
});
|
|
389
|
+
return [message];
|
|
390
|
+
}
|
|
391
|
+
|
|
298
392
|
|
|
299
393
|
/** Aggregated data awaiting user validation. */
|
|
300
394
|
public get pendingValidations(): ReadonlyMap<string, PendingValidation> {
|
|
@@ -310,9 +404,9 @@ export class AggregationParticipant {
|
|
|
310
404
|
/**
|
|
311
405
|
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
312
406
|
* Unlike {@link pendingValidations} (which lists only cohorts still awaiting
|
|
313
|
-
* the validate decision), this returns the stored validation
|
|
314
|
-
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
|
|
315
|
-
*
|
|
407
|
+
* the validate decision), this returns the stored validation, including the
|
|
408
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
|
|
409
|
+
* so it is still readable once the cohort reaches Complete. Returns
|
|
316
410
|
* undefined before aggregated data has been received.
|
|
317
411
|
*/
|
|
318
412
|
public getValidation(cohortId: string): PendingValidation | undefined {
|
|
@@ -323,8 +417,13 @@ export class AggregationParticipant {
|
|
|
323
417
|
const cohortId = message.body?.cohortId;
|
|
324
418
|
if(!cohortId) return;
|
|
325
419
|
const state = this.#cohortStates.get(cohortId);
|
|
326
|
-
|
|
327
|
-
|
|
420
|
+
// A submitter is in UpdateSubmitted; a decliner (cooperative non-inclusion)
|
|
421
|
+
// is in NonIncluded. Both validate their own slot in the distributed data.
|
|
422
|
+
if(!state || (state.phase !== ParticipantCohortPhase.UpdateSubmitted && state.phase !== ParticipantCohortPhase.NonIncluded)) return;
|
|
423
|
+
|
|
424
|
+
const declined = state.included === false;
|
|
425
|
+
// A submitter must have its update stored; a decliner has none by design.
|
|
426
|
+
if(!declined && !state.submittedUpdate) return;
|
|
328
427
|
|
|
329
428
|
const beaconType = message.body?.beaconType;
|
|
330
429
|
if(!beaconType) return;
|
|
@@ -332,11 +431,14 @@ export class AggregationParticipant {
|
|
|
332
431
|
if(!strategy) return;
|
|
333
432
|
|
|
334
433
|
const signalBytesHex = message.body?.signalBytesHex ?? '';
|
|
335
|
-
|
|
434
|
+
// Decliner validates its non-inclusion slot (CAS absence / SMT non-inclusion
|
|
435
|
+
// proof); submitter validates inclusion against its update hash.
|
|
436
|
+
const expectedHash = declined ? '' : canonicalHash(state.submittedUpdate!);
|
|
336
437
|
const result = strategy.validateParticipantView({
|
|
337
438
|
participantDid : this.did,
|
|
338
|
-
|
|
339
|
-
|
|
439
|
+
included : !declined,
|
|
440
|
+
submittedUpdate : declined ? undefined : state.submittedUpdate,
|
|
441
|
+
expectedHash : declined ? undefined : expectedHash,
|
|
340
442
|
body : message.body!,
|
|
341
443
|
});
|
|
342
444
|
|
|
@@ -348,6 +450,7 @@ export class AggregationParticipant {
|
|
|
348
450
|
matches : result.matches,
|
|
349
451
|
casAnnouncement : result.casAnnouncement,
|
|
350
452
|
smtProof : result.smtProof,
|
|
453
|
+
included : !declined,
|
|
351
454
|
};
|
|
352
455
|
state.phase = ParticipantCohortPhase.AwaitingValidation;
|
|
353
456
|
}
|
|
@@ -427,6 +530,30 @@ export class AggregationParticipant {
|
|
|
427
530
|
state.phase = ParticipantCohortPhase.AwaitingSigning;
|
|
428
531
|
}
|
|
429
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Bind a signing approval to the announcement the member validated: a beacon
|
|
535
|
+
* transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
|
|
536
|
+
* the aggregated data was distributed. Both the optimistic nonce approval and
|
|
537
|
+
* the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
|
|
538
|
+
* while the coordinator drives output selection, so without this check a
|
|
539
|
+
* coordinator could anchor a different signal under the member's signature.
|
|
540
|
+
*/
|
|
541
|
+
#assertTxAnchorsValidatedSignal(cohortId: string, state: ParticipantCohortState, tx: Transaction): void {
|
|
542
|
+
const signalHex = state.validation?.signalBytesHex;
|
|
543
|
+
if(!signalHex) {
|
|
544
|
+
throw new AggregationParticipantError(
|
|
545
|
+
`Cohort ${cohortId} has no validated signal to bind the signature to.`,
|
|
546
|
+
'MISSING_STATE', { cohortId }
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if(!txEmbedsSignal(tx, signalHex)) {
|
|
550
|
+
throw new AggregationParticipantError(
|
|
551
|
+
`Transaction for cohort ${cohortId} does not anchor the validated signal.`,
|
|
552
|
+
'SIGNAL_MISMATCH', { cohortId }
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
430
557
|
/**
|
|
431
558
|
* User action: approve signing and generate nonce contribution.
|
|
432
559
|
*/
|
|
@@ -445,7 +572,13 @@ export class AggregationParticipant {
|
|
|
445
572
|
);
|
|
446
573
|
}
|
|
447
574
|
|
|
448
|
-
|
|
575
|
+
// allowUnknownOutputs: a beacon transaction carries an OP_RETURN signal
|
|
576
|
+
// output, which scure does not classify as a known (spendable) output type;
|
|
577
|
+
// re-parsing the raw tx would otherwise throw.
|
|
578
|
+
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
|
|
579
|
+
|
|
580
|
+
// Refuse to sign unless the tx anchors the signal this member validated.
|
|
581
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
449
582
|
|
|
450
583
|
// Derive UTXO metadata for Taproot sighash (BIP-341). Use the script
|
|
451
584
|
// supplied by the service in AUTHORIZATION_REQUEST rather than reading
|
|
@@ -495,7 +628,7 @@ export class AggregationParticipant {
|
|
|
495
628
|
|
|
496
629
|
/**
|
|
497
630
|
* User action: generate and return the partial signature.
|
|
498
|
-
* In most UIs this is automatic after AwaitingPartialSig
|
|
631
|
+
* In most UIs this is automatic after AwaitingPartialSig, but exposing it
|
|
499
632
|
* as an explicit action lets the client UI confirm before signing if desired.
|
|
500
633
|
*/
|
|
501
634
|
public generatePartialSignature(cohortId: string): BaseMessage[] {
|
|
@@ -529,6 +662,105 @@ export class AggregationParticipant {
|
|
|
529
662
|
}
|
|
530
663
|
|
|
531
664
|
|
|
665
|
+
/** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
|
|
666
|
+
public get pendingFallbackRequests(): ReadonlyMap<string, PendingFallbackRequest> {
|
|
667
|
+
const map = new Map<string, PendingFallbackRequest>();
|
|
668
|
+
for(const [id, state] of this.#cohortStates) {
|
|
669
|
+
if(state.phase === ParticipantCohortPhase.AwaitingFallbackSig && state.fallbackRequest) {
|
|
670
|
+
map.set(id, state.fallbackRequest);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return map;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#handleFallbackAuthorizationRequest(message: BaseMessage): void {
|
|
677
|
+
const cohortId = message.body?.cohortId;
|
|
678
|
+
if(!cohortId) return;
|
|
679
|
+
const state = this.#cohortStates.get(cohortId);
|
|
680
|
+
if(!state || !state.cohort) return;
|
|
681
|
+
// The service can fall back at any point after the member validated. This
|
|
682
|
+
// includes the local Complete phase a member reaches the moment it sends its
|
|
683
|
+
// optimistic partial signature: the cohort has NOT finalized (the service
|
|
684
|
+
// only falls back before optimistic completion), and those members are
|
|
685
|
+
// exactly the k signers the fallback needs. Signing both the optimistic
|
|
686
|
+
// partial sig and the fallback sig is safe - both authorize the same outputs,
|
|
687
|
+
// and only one witness can ever confirm the single UTXO. A genuinely failed
|
|
688
|
+
// member is excluded. Ignore a duplicate request already being processed.
|
|
689
|
+
const acceptFrom: ParticipantCohortPhaseType[] = [
|
|
690
|
+
ParticipantCohortPhase.ValidationSent,
|
|
691
|
+
ParticipantCohortPhase.AwaitingSigning,
|
|
692
|
+
ParticipantCohortPhase.NonceSent,
|
|
693
|
+
ParticipantCohortPhase.AwaitingPartialSig,
|
|
694
|
+
ParticipantCohortPhase.Complete,
|
|
695
|
+
];
|
|
696
|
+
if(!acceptFrom.includes(state.phase)) return;
|
|
697
|
+
|
|
698
|
+
const sessionId = message.body?.sessionId;
|
|
699
|
+
const pendingTxHex = message.body?.pendingTx;
|
|
700
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
701
|
+
const prevOutValue = message.body?.prevOutValue;
|
|
702
|
+
const fallbackLeafScriptHex = message.body?.fallbackLeafScriptHex;
|
|
703
|
+
if(!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
|
|
704
|
+
|
|
705
|
+
state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
|
|
706
|
+
// The optimistic path is abandoned; wipe any retained secret nonce for it.
|
|
707
|
+
state.signingSession?.clearSecrets();
|
|
708
|
+
state.phase = ParticipantCohortPhase.AwaitingFallbackSig;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* User action: authorize the fallback spend. Recomputes the k-of-n fallback
|
|
713
|
+
* leaf from the member's OWN cohort state (not the service-provided script),
|
|
714
|
+
* computes the BIP-341 script-path sighash over the requested transaction, and
|
|
715
|
+
* returns a standalone BIP-340 signature (no nonce round). The member completes
|
|
716
|
+
* once it has contributed; the service needs only k of these.
|
|
717
|
+
*/
|
|
718
|
+
public approveFallback(cohortId: string): BaseMessage[] {
|
|
719
|
+
const state = this.#cohortStates.get(cohortId);
|
|
720
|
+
if(!state || state.phase !== ParticipantCohortPhase.AwaitingFallbackSig) {
|
|
721
|
+
throw new AggregationParticipantError(
|
|
722
|
+
`Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
|
|
723
|
+
'INVALID_PHASE', { cohortId, phase: state?.phase }
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if(!state.fallbackRequest || !state.cohort) {
|
|
727
|
+
throw new AggregationParticipantError(
|
|
728
|
+
`Cohort ${cohortId} missing fallback request or cohort state.`,
|
|
729
|
+
'MISSING_STATE', { cohortId }
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const req = state.fallbackRequest;
|
|
734
|
+
const tx = Transaction.fromRaw(hexToBytes(req.pendingTxHex), { allowUnknownOutputs: true });
|
|
735
|
+
const prevOutScript = hexToBytes(req.prevOutScriptHex);
|
|
736
|
+
const prevOutValue = BigInt(req.prevOutValue);
|
|
737
|
+
|
|
738
|
+
// Refuse to sign unless the fallback tx anchors the signal this member
|
|
739
|
+
// validated (the coordinator drives output selection on the fallback path).
|
|
740
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
741
|
+
|
|
742
|
+
// Recompute the fallback leaf from our own cohort keys so a malicious service
|
|
743
|
+
// cannot induce a signature over a different leaf than the one the funded
|
|
744
|
+
// address commits to.
|
|
745
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
746
|
+
cohortKeys : state.cohort.cohortKeys,
|
|
747
|
+
fallbackThreshold : state.cohort.effectiveFallbackThreshold,
|
|
748
|
+
});
|
|
749
|
+
const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
750
|
+
const signature = this.#signer.withSecret(secretKey => schnorr.sign(sighash, secretKey));
|
|
751
|
+
|
|
752
|
+
state.phase = ParticipantCohortPhase.Complete;
|
|
753
|
+
return [createFallbackSignatureMessage({
|
|
754
|
+
from : this.did,
|
|
755
|
+
to : state.serviceDid,
|
|
756
|
+
cohortId,
|
|
757
|
+
sessionId : req.sessionId,
|
|
758
|
+
signerPk : this.publicKey.slice(1),
|
|
759
|
+
fallbackSignature : signature,
|
|
760
|
+
})];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
|
|
532
764
|
public getCohortPhase(cohortId: string): ParticipantCohortPhaseType | undefined {
|
|
533
765
|
return this.#cohortStates.get(cohortId)?.phase;
|
|
534
766
|
}
|
|
@@ -19,6 +19,7 @@ export type ServiceCohortPhaseType =
|
|
|
19
19
|
| 'SigningStarted'
|
|
20
20
|
| 'NoncesCollected'
|
|
21
21
|
| 'AwaitingPartialSigs'
|
|
22
|
+
| 'FallbackRequested'
|
|
22
23
|
| 'Complete'
|
|
23
24
|
| 'Failed';
|
|
24
25
|
|
|
@@ -33,6 +34,8 @@ export enum ServiceCohortPhase {
|
|
|
33
34
|
SigningStarted = 'SigningStarted',
|
|
34
35
|
NoncesCollected = 'NoncesCollected',
|
|
35
36
|
AwaitingPartialSigs = 'AwaitingPartialSigs',
|
|
37
|
+
/** Optimistic n-of-n key path abandoned; collecting k-of-n fallback signatures (ADR 042). */
|
|
38
|
+
FallbackRequested = 'FallbackRequested',
|
|
36
39
|
Complete = 'Complete',
|
|
37
40
|
Failed = 'Failed',
|
|
38
41
|
}
|
|
@@ -42,11 +45,13 @@ export type ParticipantCohortPhaseType =
|
|
|
42
45
|
| 'OptedIn'
|
|
43
46
|
| 'CohortReady'
|
|
44
47
|
| 'UpdateSubmitted'
|
|
48
|
+
| 'NonIncluded'
|
|
45
49
|
| 'AwaitingValidation'
|
|
46
50
|
| 'ValidationSent'
|
|
47
51
|
| 'AwaitingSigning'
|
|
48
52
|
| 'NonceSent'
|
|
49
53
|
| 'AwaitingPartialSig'
|
|
54
|
+
| 'AwaitingFallbackSig'
|
|
50
55
|
| 'Complete'
|
|
51
56
|
| 'Failed';
|
|
52
57
|
|
|
@@ -55,11 +60,15 @@ export enum ParticipantCohortPhase {
|
|
|
55
60
|
OptedIn = 'OptedIn',
|
|
56
61
|
CohortReady = 'CohortReady',
|
|
57
62
|
UpdateSubmitted = 'UpdateSubmitted',
|
|
63
|
+
/** Member declined to submit an update this round (cooperative non-inclusion); still signs. */
|
|
64
|
+
NonIncluded = 'NonIncluded',
|
|
58
65
|
AwaitingValidation = 'AwaitingValidation',
|
|
59
66
|
ValidationSent = 'ValidationSent',
|
|
60
67
|
AwaitingSigning = 'AwaitingSigning',
|
|
61
68
|
NonceSent = 'NonceSent',
|
|
62
69
|
AwaitingPartialSig = 'AwaitingPartialSig',
|
|
70
|
+
/** Service fell back to the k-of-n script path; member can sign the fallback (ADR 042). */
|
|
71
|
+
AwaitingFallbackSig = 'AwaitingFallbackSig',
|
|
63
72
|
Complete = 'Complete',
|
|
64
73
|
Failed = 'Failed',
|
|
65
74
|
}
|