@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
package/dist/browser.mjs
CHANGED
|
@@ -100123,7 +100123,7 @@ var SchnorrMultikey = class _SchnorrMultikey {
|
|
|
100123
100123
|
* @readonly
|
|
100124
100124
|
* Get signing ability of the Multikey: true if a local Secp256k1SecretKey
|
|
100125
100125
|
* is present (note: the SchnorrKeyPair.secretKey getter throws when absent;
|
|
100126
|
-
* that throw is the historical error contract
|
|
100126
|
+
* that throw is the historical error contract - see multikey tests) or if
|
|
100127
100127
|
* an external signer is available.
|
|
100128
100128
|
*/
|
|
100129
100129
|
get signer() {
|
|
@@ -100228,6 +100228,7 @@ var SchnorrMultikey = class _SchnorrMultikey {
|
|
|
100228
100228
|
init_shim();
|
|
100229
100229
|
|
|
100230
100230
|
// src/core/aggregation/service.ts
|
|
100231
|
+
init_secp256k1();
|
|
100231
100232
|
init_utils();
|
|
100232
100233
|
|
|
100233
100234
|
// src/core/aggregation/beacon-strategy.ts
|
|
@@ -100528,9 +100529,12 @@ var CAS_STRATEGY = {
|
|
|
100528
100529
|
getDistributePayload(cohort) {
|
|
100529
100530
|
return { casAnnouncement: cohort.casAnnouncement };
|
|
100530
100531
|
},
|
|
100531
|
-
validateParticipantView({ participantDid, expectedHash, body }) {
|
|
100532
|
+
validateParticipantView({ participantDid, included, expectedHash, body }) {
|
|
100532
100533
|
const casAnnouncement = body.casAnnouncement;
|
|
100533
100534
|
if (!casAnnouncement) return { matches: false };
|
|
100535
|
+
if (!included) {
|
|
100536
|
+
return { matches: !(participantDid in casAnnouncement), casAnnouncement };
|
|
100537
|
+
}
|
|
100534
100538
|
return {
|
|
100535
100539
|
matches: casAnnouncement[participantDid] === expectedHash,
|
|
100536
100540
|
casAnnouncement
|
|
@@ -100546,15 +100550,20 @@ var SMT_STRATEGY = {
|
|
|
100546
100550
|
const proof = cohort.smtProofs?.get(participantDid);
|
|
100547
100551
|
return { smtProof: proof };
|
|
100548
100552
|
},
|
|
100549
|
-
validateParticipantView({ participantDid, submittedUpdate, body }) {
|
|
100553
|
+
validateParticipantView({ participantDid, included, submittedUpdate, body }) {
|
|
100550
100554
|
const smtProof = body.smtProof;
|
|
100551
|
-
|
|
100555
|
+
const index = didToIndex(participantDid);
|
|
100556
|
+
if (!included) {
|
|
100557
|
+
if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
|
|
100558
|
+
const candidateHash2 = blockHash(blockHash(base64UrlToHash(smtProof.nonce)));
|
|
100559
|
+
return { matches: verifySerializedProof(smtProof, index, candidateHash2), smtProof };
|
|
100560
|
+
}
|
|
100561
|
+
if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
|
|
100552
100562
|
const canonicalBytes = new TextEncoder().encode(canonicalize2(submittedUpdate));
|
|
100553
100563
|
const expectedUpdateId = hashToBase64Url(blockHash(canonicalBytes));
|
|
100554
100564
|
if (smtProof.updateId !== expectedUpdateId) {
|
|
100555
100565
|
return { matches: false, smtProof };
|
|
100556
100566
|
}
|
|
100557
|
-
const index = didToIndex(participantDid);
|
|
100558
100567
|
const candidateHash = blockHash(blockHash(base64UrlToHash(smtProof.nonce)), base64UrlToHash(smtProof.updateId));
|
|
100559
100568
|
return {
|
|
100560
100569
|
matches: verifySerializedProof(smtProof, index, candidateHash),
|
|
@@ -101553,6 +101562,15 @@ function checkScript(script, redeemScript, witnessScript) {
|
|
|
101553
101562
|
checkWSH(r2, witnessScript);
|
|
101554
101563
|
}
|
|
101555
101564
|
}
|
|
101565
|
+
function uniqPubkey(pubkeys) {
|
|
101566
|
+
const map3 = {};
|
|
101567
|
+
for (const pub of pubkeys) {
|
|
101568
|
+
const key = hex.encode(pub);
|
|
101569
|
+
if (map3[key])
|
|
101570
|
+
throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
|
|
101571
|
+
map3[key] = true;
|
|
101572
|
+
}
|
|
101573
|
+
}
|
|
101556
101574
|
var p2pkh = (publicKey2, network = NETWORK) => {
|
|
101557
101575
|
if (!isValidPubkey(publicKey2, PubT.ecdsa))
|
|
101558
101576
|
throw new Error("P2PKH: invalid publicKey");
|
|
@@ -101725,6 +101743,14 @@ function p2tr(internalPubKey, tree2, network = NETWORK, allowUnknownOutputs = fa
|
|
|
101725
101743
|
};
|
|
101726
101744
|
}
|
|
101727
101745
|
}
|
|
101746
|
+
function p2tr_ms(m2, pubkeys, allowSamePubkeys = false) {
|
|
101747
|
+
if (!allowSamePubkeys)
|
|
101748
|
+
uniqPubkey(pubkeys);
|
|
101749
|
+
return {
|
|
101750
|
+
type: "tr_ms",
|
|
101751
|
+
script: OutScript.encode({ type: "tr_ms", pubkeys, m: m2 })
|
|
101752
|
+
};
|
|
101753
|
+
}
|
|
101728
101754
|
var base58check = createBase58check(sha256);
|
|
101729
101755
|
function validateWitness(version3, data) {
|
|
101730
101756
|
if (data.length < 2 || data.length > 40)
|
|
@@ -103200,6 +103226,104 @@ var SigningSessionError = class extends MethodError {
|
|
|
103200
103226
|
}
|
|
103201
103227
|
};
|
|
103202
103228
|
|
|
103229
|
+
// src/core/aggregation/recovery-policy.ts
|
|
103230
|
+
init_shim();
|
|
103231
|
+
init_secp256k1();
|
|
103232
|
+
init_utils();
|
|
103233
|
+
var DEFAULT_FUNDING_MODEL = "operator-funded";
|
|
103234
|
+
var DEFAULT_RECOVERY_SEQUENCE = 144;
|
|
103235
|
+
var MAX_RECOVERY_SEQUENCE = 65535;
|
|
103236
|
+
var TAPROOT_LEAF_VERSION = 192;
|
|
103237
|
+
function compactSize(n2) {
|
|
103238
|
+
if (n2 < 253) return new Uint8Array([n2]);
|
|
103239
|
+
if (n2 <= 65535) return new Uint8Array([253, n2 & 255, n2 >> 8 & 255]);
|
|
103240
|
+
return new Uint8Array([254, n2 & 255, n2 >> 8 & 255, n2 >> 16 & 255, n2 >> 24 & 255]);
|
|
103241
|
+
}
|
|
103242
|
+
function tapLeafHash2(script, leafVersion = TAPROOT_LEAF_VERSION) {
|
|
103243
|
+
return schnorr2.utils.taggedHash(
|
|
103244
|
+
"TapLeaf",
|
|
103245
|
+
concatBytes(new Uint8Array([leafVersion]), compactSize(script.length), script)
|
|
103246
|
+
);
|
|
103247
|
+
}
|
|
103248
|
+
function resolveFallbackThreshold(advertised, n2) {
|
|
103249
|
+
return advertised ?? Math.max(1, n2 - 1);
|
|
103250
|
+
}
|
|
103251
|
+
function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
|
|
103252
|
+
const n2 = cohortKeys.length;
|
|
103253
|
+
if (n2 === 0) {
|
|
103254
|
+
throw new AggregationCohortError(
|
|
103255
|
+
"Cannot build fallback leaf: no cohort keys.",
|
|
103256
|
+
"NO_COHORT_KEYS"
|
|
103257
|
+
);
|
|
103258
|
+
}
|
|
103259
|
+
for (const key of cohortKeys) {
|
|
103260
|
+
if (key.length !== 33) {
|
|
103261
|
+
throw new AggregationCohortError(
|
|
103262
|
+
`Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
|
|
103263
|
+
"INVALID_COHORT_KEY",
|
|
103264
|
+
{ length: key.length }
|
|
103265
|
+
);
|
|
103266
|
+
}
|
|
103267
|
+
}
|
|
103268
|
+
if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n2) {
|
|
103269
|
+
throw new AggregationCohortError(
|
|
103270
|
+
`Fallback threshold must be an integer in [1, ${n2}] (k-of-n), got ${fallbackThreshold}.`,
|
|
103271
|
+
"INVALID_FALLBACK_THRESHOLD",
|
|
103272
|
+
{ fallbackThreshold, n: n2 }
|
|
103273
|
+
);
|
|
103274
|
+
}
|
|
103275
|
+
}
|
|
103276
|
+
function buildFallbackLeaf(params) {
|
|
103277
|
+
assertFallbackParams(params);
|
|
103278
|
+
const xOnlyKeys = sortKeys(params.cohortKeys).map((k) => k.slice(1));
|
|
103279
|
+
return p2tr_ms(params.fallbackThreshold, xOnlyKeys).script;
|
|
103280
|
+
}
|
|
103281
|
+
function assertRecoveryParams({ recoveryKey, recoverySequence }) {
|
|
103282
|
+
if (recoveryKey.length !== 32) {
|
|
103283
|
+
throw new AggregationCohortError(
|
|
103284
|
+
`Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
|
|
103285
|
+
"INVALID_RECOVERY_KEY"
|
|
103286
|
+
);
|
|
103287
|
+
}
|
|
103288
|
+
if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103289
|
+
throw new AggregationCohortError(
|
|
103290
|
+
`Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
|
|
103291
|
+
"INVALID_RECOVERY_SEQUENCE"
|
|
103292
|
+
);
|
|
103293
|
+
}
|
|
103294
|
+
}
|
|
103295
|
+
function buildRecoveryScript(params) {
|
|
103296
|
+
assertRecoveryParams(params);
|
|
103297
|
+
return Script.encode([
|
|
103298
|
+
params.recoverySequence,
|
|
103299
|
+
"CHECKSEQUENCEVERIFY",
|
|
103300
|
+
"DROP",
|
|
103301
|
+
params.recoveryKey,
|
|
103302
|
+
"CHECKSIG"
|
|
103303
|
+
]);
|
|
103304
|
+
}
|
|
103305
|
+
function buildRecoveryLeaves(fundingModel, params) {
|
|
103306
|
+
switch (fundingModel) {
|
|
103307
|
+
case "operator-funded":
|
|
103308
|
+
return [
|
|
103309
|
+
{ script: buildFallbackLeaf(params) },
|
|
103310
|
+
{ script: buildRecoveryScript(params) }
|
|
103311
|
+
];
|
|
103312
|
+
case "participant-funded":
|
|
103313
|
+
throw new AggregationCohortError(
|
|
103314
|
+
"Funding model 'participant-funded' is reserved and not yet implemented.",
|
|
103315
|
+
"UNSUPPORTED_FUNDING_MODEL",
|
|
103316
|
+
{ fundingModel }
|
|
103317
|
+
);
|
|
103318
|
+
default:
|
|
103319
|
+
throw new AggregationCohortError(
|
|
103320
|
+
`Unknown funding model: ${fundingModel}.`,
|
|
103321
|
+
"UNKNOWN_FUNDING_MODEL",
|
|
103322
|
+
{ fundingModel }
|
|
103323
|
+
);
|
|
103324
|
+
}
|
|
103325
|
+
}
|
|
103326
|
+
|
|
103203
103327
|
// src/core/aggregation/cohort.ts
|
|
103204
103328
|
var AggregationCohort = class {
|
|
103205
103329
|
/** Unique identifier for the cohort. */
|
|
@@ -103215,8 +103339,8 @@ var AggregationCohort = class {
|
|
|
103215
103339
|
/** List of participant DIDs that have been accepted into the cohort. */
|
|
103216
103340
|
participants = [];
|
|
103217
103341
|
/**
|
|
103218
|
-
* Mapping from participant DID
|
|
103219
|
-
* Distinct from {@link cohortKeys} (which is sorted per BIP-327)
|
|
103342
|
+
* Mapping from participant DID to their compressed secp256k1 public key.
|
|
103343
|
+
* Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
|
|
103220
103344
|
* callers look up a participant's key without knowing their position in the
|
|
103221
103345
|
* sorted array. Populated by the service at `acceptParticipant` time.
|
|
103222
103346
|
*/
|
|
@@ -103224,16 +103348,41 @@ var AggregationCohort = class {
|
|
|
103224
103348
|
/** Sorted list of cohort participants' compressed public keys. */
|
|
103225
103349
|
#cohortKeys = [];
|
|
103226
103350
|
/**
|
|
103227
|
-
* BIP-341 TapTweak
|
|
103228
|
-
*
|
|
103229
|
-
*
|
|
103351
|
+
* BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
|
|
103352
|
+
* The beacon output is an internal key (the MuSig2 aggregate) plus a script
|
|
103353
|
+
* tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
|
|
103354
|
+
* The MuSig2 signing session applies this as an x-only tweak; a value that does
|
|
103355
|
+
* not match the root the funded address was derived from silently yields an
|
|
103356
|
+
* invalid key-path signature.
|
|
103230
103357
|
*/
|
|
103231
103358
|
tapTweak = new Uint8Array();
|
|
103232
|
-
/** The n-of-n MuSig2
|
|
103359
|
+
/** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
|
|
103360
|
+
internalKey = new Uint8Array();
|
|
103361
|
+
/** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
|
|
103362
|
+
tapMerkleRoot = new Uint8Array();
|
|
103363
|
+
/** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
|
|
103364
|
+
recoveryKey;
|
|
103365
|
+
/** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
|
|
103366
|
+
recoverySequence;
|
|
103367
|
+
/** Funding model governing the recovery leaves (default 'operator-funded'). */
|
|
103368
|
+
fundingModel;
|
|
103369
|
+
/**
|
|
103370
|
+
* Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
|
|
103371
|
+
* address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
|
|
103372
|
+
*/
|
|
103373
|
+
fallbackThreshold;
|
|
103374
|
+
/** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
|
|
103233
103375
|
beaconAddress = "";
|
|
103234
103376
|
/** Pending DID updates submitted by participants, keyed by DID. */
|
|
103235
103377
|
pendingUpdates = /* @__PURE__ */ new Map();
|
|
103236
|
-
/**
|
|
103378
|
+
/**
|
|
103379
|
+
* Participant DIDs that explicitly declined to submit an update this round
|
|
103380
|
+
* (cooperative non-inclusion). A decliner is absent from the CAS Announcement
|
|
103381
|
+
* Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
|
|
103382
|
+
* disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
|
|
103383
|
+
*/
|
|
103384
|
+
nonIncluded = /* @__PURE__ */ new Set();
|
|
103385
|
+
/** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
|
|
103237
103386
|
casAnnouncement;
|
|
103238
103387
|
/** Per-participant SMT proofs, set by buildSMTTree(). */
|
|
103239
103388
|
smtProofs;
|
|
@@ -103243,12 +103392,16 @@ var AggregationCohort = class {
|
|
|
103243
103392
|
validationAcks = /* @__PURE__ */ new Set();
|
|
103244
103393
|
/** Set of participant DIDs that have rejected the aggregated data. */
|
|
103245
103394
|
validationRejections = /* @__PURE__ */ new Set();
|
|
103246
|
-
constructor({ id, minParticipants, serviceDid, network, beaconType }) {
|
|
103395
|
+
constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
|
|
103247
103396
|
this.id = id || crypto.randomUUID();
|
|
103248
103397
|
this.minParticipants = minParticipants ?? 2;
|
|
103249
103398
|
this.serviceDid = serviceDid || "";
|
|
103250
103399
|
this.network = network;
|
|
103251
103400
|
this.beaconType = beaconType || "CASBeacon";
|
|
103401
|
+
this.recoveryKey = recoveryKey;
|
|
103402
|
+
this.recoverySequence = recoverySequence;
|
|
103403
|
+
this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
|
|
103404
|
+
this.fallbackThreshold = fallbackThreshold;
|
|
103252
103405
|
}
|
|
103253
103406
|
/** Sorted cohort keys (sorted on assignment per BIP-327). */
|
|
103254
103407
|
get cohortKeys() {
|
|
@@ -103258,8 +103411,26 @@ var AggregationCohort = class {
|
|
|
103258
103411
|
this.#cohortKeys = sortKeys(keys);
|
|
103259
103412
|
}
|
|
103260
103413
|
/**
|
|
103261
|
-
*
|
|
103262
|
-
*
|
|
103414
|
+
* The resolved k of the k-of-n fallback leaf: the advertised
|
|
103415
|
+
* {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
|
|
103416
|
+
* the current cohort size. This is the value the beacon address commits to and
|
|
103417
|
+
* the spend builders must reproduce. Returns 0 before any cohort keys are set.
|
|
103418
|
+
*/
|
|
103419
|
+
get effectiveFallbackThreshold() {
|
|
103420
|
+
if (this.#cohortKeys.length === 0) return 0;
|
|
103421
|
+
return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
|
|
103422
|
+
}
|
|
103423
|
+
/**
|
|
103424
|
+
* Computes the Taproot beacon address from the cohort keys and recovery params.
|
|
103425
|
+
*
|
|
103426
|
+
* The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
|
|
103427
|
+
* script path is the recovery tree (a CSV recovery leaf so a missing signer
|
|
103428
|
+
* cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
|
|
103429
|
+
* the `tapTweak` the MuSig2 session must apply for the key-path spend.
|
|
103430
|
+
*
|
|
103431
|
+
* The tweak is derived from the Merkle root the address was built with (read
|
|
103432
|
+
* back from the payment), never recomputed by hand, so the MuSig2 key-path
|
|
103433
|
+
* signature is guaranteed to validate against the funded address.
|
|
103263
103434
|
*/
|
|
103264
103435
|
computeBeaconAddress() {
|
|
103265
103436
|
if (this.#cohortKeys.length === 0) {
|
|
@@ -103269,10 +103440,25 @@ var AggregationCohort = class {
|
|
|
103269
103440
|
{ cohortId: this.id }
|
|
103270
103441
|
);
|
|
103271
103442
|
}
|
|
103443
|
+
if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
|
|
103444
|
+
throw new AggregationCohortError(
|
|
103445
|
+
"Cannot compute beacon address: missing recovery key or sequence.",
|
|
103446
|
+
"NO_RECOVERY_PARAMS",
|
|
103447
|
+
{ cohortId: this.id }
|
|
103448
|
+
);
|
|
103449
|
+
}
|
|
103272
103450
|
const keyAggContext = keyAggregate(this.#cohortKeys);
|
|
103273
103451
|
const aggPubkey = keyAggExport(keyAggContext);
|
|
103274
|
-
const
|
|
103275
|
-
|
|
103452
|
+
const leaves = buildRecoveryLeaves(this.fundingModel, {
|
|
103453
|
+
recoveryKey: this.recoveryKey,
|
|
103454
|
+
recoverySequence: this.recoverySequence,
|
|
103455
|
+
cohortKeys: this.#cohortKeys,
|
|
103456
|
+
fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
|
|
103457
|
+
});
|
|
103458
|
+
const payment = p2tr(aggPubkey, leaves, getNetwork(this.network), true);
|
|
103459
|
+
this.internalKey = aggPubkey;
|
|
103460
|
+
this.tapMerkleRoot = payment.tapMerkleRoot;
|
|
103461
|
+
this.tapTweak = schnorr2.utils.taggedHash("TapTweak", concatBytes(aggPubkey, payment.tapMerkleRoot));
|
|
103276
103462
|
if (!payment.address) {
|
|
103277
103463
|
throw new AggregationCohortError(
|
|
103278
103464
|
"Failed to compute Taproot address",
|
|
@@ -103328,20 +103514,54 @@ var AggregationCohort = class {
|
|
|
103328
103514
|
}
|
|
103329
103515
|
this.pendingUpdates.set(participantDid, signedUpdate);
|
|
103330
103516
|
}
|
|
103517
|
+
/**
|
|
103518
|
+
* Record that a participant declined to submit an update this round
|
|
103519
|
+
* (cooperative non-inclusion). The member stays in the cohort and still signs.
|
|
103520
|
+
*/
|
|
103521
|
+
addNonInclusion(participantDid) {
|
|
103522
|
+
if (!this.participants.includes(participantDid)) {
|
|
103523
|
+
throw new AggregationCohortError(
|
|
103524
|
+
`Participant ${participantDid} is not in cohort ${this.id}.`,
|
|
103525
|
+
"UNKNOWN_PARTICIPANT",
|
|
103526
|
+
{ cohortId: this.id, participantDid }
|
|
103527
|
+
);
|
|
103528
|
+
}
|
|
103529
|
+
if (this.pendingUpdates.has(participantDid)) {
|
|
103530
|
+
throw new AggregationCohortError(
|
|
103531
|
+
`Participant ${participantDid} already submitted an update; cannot also decline.`,
|
|
103532
|
+
"CONFLICTING_RESPONSE",
|
|
103533
|
+
{ cohortId: this.id, participantDid }
|
|
103534
|
+
);
|
|
103535
|
+
}
|
|
103536
|
+
this.nonIncluded.add(participantDid);
|
|
103537
|
+
}
|
|
103331
103538
|
hasAllUpdates() {
|
|
103332
103539
|
return this.pendingUpdates.size === this.participants.length;
|
|
103333
103540
|
}
|
|
103541
|
+
/**
|
|
103542
|
+
* True when every participant has responded for this round, either with an
|
|
103543
|
+
* update or with an explicit non-inclusion. This is the aggregation gate when
|
|
103544
|
+
* non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
|
|
103545
|
+
* {@link hasAllValidationResponses} generalizes a unanimous ack.
|
|
103546
|
+
*/
|
|
103547
|
+
hasAllResponses() {
|
|
103548
|
+
return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
|
|
103549
|
+
}
|
|
103334
103550
|
/**
|
|
103335
103551
|
* Builds a CAS Announcement Map from collected updates.
|
|
103336
|
-
* Maps each participant DID
|
|
103552
|
+
* Maps each participant DID to base64url canonical hash of their signed update.
|
|
103337
103553
|
* Computes signal bytes as SHA-256 of canonicalized announcement.
|
|
103554
|
+
*
|
|
103555
|
+
* Members who declined (cooperative non-inclusion) are naturally absent from
|
|
103556
|
+
* the map: the body iterates {@link pendingUpdates}, which never holds a
|
|
103557
|
+
* decliner. Absence from the map is exactly the CAS non-inclusion signal.
|
|
103338
103558
|
*/
|
|
103339
103559
|
buildCASAnnouncement() {
|
|
103340
|
-
if (!this.
|
|
103560
|
+
if (!this.hasAllResponses()) {
|
|
103341
103561
|
throw new AggregationCohortError(
|
|
103342
|
-
"Cannot build CAS Announcement: not all
|
|
103343
|
-
"
|
|
103344
|
-
{ cohortId: this.id,
|
|
103562
|
+
"Cannot build CAS Announcement: not all participants have responded.",
|
|
103563
|
+
"INCOMPLETE_RESPONSES",
|
|
103564
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103345
103565
|
);
|
|
103346
103566
|
}
|
|
103347
103567
|
const announcement = {};
|
|
@@ -103353,31 +103573,41 @@ var AggregationCohort = class {
|
|
|
103353
103573
|
return announcement;
|
|
103354
103574
|
}
|
|
103355
103575
|
/**
|
|
103356
|
-
* Builds an SMT tree
|
|
103357
|
-
*
|
|
103358
|
-
*
|
|
103576
|
+
* Builds an SMT tree with one leaf per participant.
|
|
103577
|
+
*
|
|
103578
|
+
* A member who submitted an update gets an inclusion leaf
|
|
103579
|
+
* (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
|
|
103580
|
+
* non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
|
|
103581
|
+
* omitted). The cohort mints each member's nonce and returns it inside that
|
|
103582
|
+
* member's serialized proof, so a decliner can self-validate its own
|
|
103583
|
+
* non-inclusion slot and the resolver can recompute the leaf. Stores
|
|
103584
|
+
* per-participant proofs and the SMT root as signalBytes.
|
|
103359
103585
|
*/
|
|
103360
103586
|
buildSMTTree() {
|
|
103361
|
-
if (!this.
|
|
103587
|
+
if (!this.hasAllResponses()) {
|
|
103362
103588
|
throw new AggregationCohortError(
|
|
103363
|
-
"Cannot build SMT tree: not all
|
|
103364
|
-
"
|
|
103365
|
-
{ cohortId: this.id }
|
|
103589
|
+
"Cannot build SMT tree: not all participants have responded.",
|
|
103590
|
+
"INCOMPLETE_RESPONSES",
|
|
103591
|
+
{ cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
|
|
103366
103592
|
);
|
|
103367
103593
|
}
|
|
103368
103594
|
const tree2 = new BTCR2MerkleTree();
|
|
103369
103595
|
const entries = [];
|
|
103370
103596
|
const encoder4 = new TextEncoder();
|
|
103371
|
-
for (const
|
|
103372
|
-
const canonicalBytes = encoder4.encode(canonicalize2(signedUpdate));
|
|
103597
|
+
for (const did of this.participants) {
|
|
103373
103598
|
const nonce = randomBytes(32);
|
|
103374
|
-
|
|
103599
|
+
const signedUpdate = this.pendingUpdates.get(did);
|
|
103600
|
+
if (signedUpdate) {
|
|
103601
|
+
entries.push({ did, nonce, signedUpdate: encoder4.encode(canonicalize2(signedUpdate)) });
|
|
103602
|
+
} else {
|
|
103603
|
+
entries.push({ did, nonce });
|
|
103604
|
+
}
|
|
103375
103605
|
}
|
|
103376
103606
|
tree2.addEntries(entries);
|
|
103377
103607
|
tree2.finalize();
|
|
103378
103608
|
this.signalBytes = tree2.rootHash;
|
|
103379
103609
|
this.smtProofs = /* @__PURE__ */ new Map();
|
|
103380
|
-
for (const
|
|
103610
|
+
for (const did of this.participants) {
|
|
103381
103611
|
this.smtProofs.set(did, tree2.proof(did));
|
|
103382
103612
|
}
|
|
103383
103613
|
return this.smtProofs;
|
|
@@ -103403,7 +103633,7 @@ var AggregationCohort = class {
|
|
|
103403
103633
|
return this.validationAcks.size + this.validationRejections.size === this.participants.length;
|
|
103404
103634
|
}
|
|
103405
103635
|
/**
|
|
103406
|
-
* True when all participants approved. Note: differs from {@link hasAllValidationResponses}
|
|
103636
|
+
* True when all participants approved. Note: differs from {@link hasAllValidationResponses},
|
|
103407
103637
|
* this returns false if any participant rejected, even if all responses are in.
|
|
103408
103638
|
*/
|
|
103409
103639
|
isFullyValidated() {
|
|
@@ -103414,6 +103644,7 @@ var AggregationCohort = class {
|
|
|
103414
103644
|
// src/core/aggregation/conditions.ts
|
|
103415
103645
|
init_shim();
|
|
103416
103646
|
var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
|
|
103647
|
+
var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
|
|
103417
103648
|
function checkPair(problems, label, min, max) {
|
|
103418
103649
|
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
103419
103650
|
problems.push(`min${label} must be an integer >= 0`);
|
|
@@ -103457,11 +103688,128 @@ function validateCohortConditions(c2) {
|
|
|
103457
103688
|
if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
|
|
103458
103689
|
problems.push("pendingUpdateTrigger must be an integer >= 1");
|
|
103459
103690
|
}
|
|
103691
|
+
if (c2.fallbackThreshold !== void 0) {
|
|
103692
|
+
if (!Number.isInteger(c2.fallbackThreshold) || c2.fallbackThreshold < 1) {
|
|
103693
|
+
problems.push("fallbackThreshold must be an integer >= 1");
|
|
103694
|
+
} else if (c2.maxParticipants !== void 0 && Number.isInteger(c2.maxParticipants) && c2.fallbackThreshold > c2.maxParticipants) {
|
|
103695
|
+
problems.push("fallbackThreshold must be <= maxParticipants");
|
|
103696
|
+
}
|
|
103697
|
+
}
|
|
103460
103698
|
checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
|
|
103461
103699
|
checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
|
|
103700
|
+
if (typeof c2.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c2.recoveryKey)) {
|
|
103701
|
+
problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
|
|
103702
|
+
}
|
|
103703
|
+
if (!Number.isInteger(c2.recoverySequence) || c2.recoverySequence < 1 || c2.recoverySequence > MAX_RECOVERY_SEQUENCE) {
|
|
103704
|
+
problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
|
|
103705
|
+
}
|
|
103706
|
+
if (c2.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c2.fundingModel)) {
|
|
103707
|
+
problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
|
|
103708
|
+
}
|
|
103462
103709
|
return problems;
|
|
103463
103710
|
}
|
|
103464
103711
|
|
|
103712
|
+
// src/core/aggregation/fallback-spend.ts
|
|
103713
|
+
init_shim();
|
|
103714
|
+
init_secp256k1();
|
|
103715
|
+
function bytesEqual(a2, b) {
|
|
103716
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
103717
|
+
}
|
|
103718
|
+
function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
|
|
103719
|
+
return tx.preimageWitnessV1(
|
|
103720
|
+
inputIndex,
|
|
103721
|
+
[prevOutScript],
|
|
103722
|
+
SigHash.DEFAULT,
|
|
103723
|
+
[prevOutValue],
|
|
103724
|
+
void 0,
|
|
103725
|
+
fallbackLeafScript,
|
|
103726
|
+
TAPROOT_LEAF_VERSION
|
|
103727
|
+
);
|
|
103728
|
+
}
|
|
103729
|
+
function buildFallbackSpend(params) {
|
|
103730
|
+
const {
|
|
103731
|
+
pendingTx,
|
|
103732
|
+
cohortKeys,
|
|
103733
|
+
recoveryKey,
|
|
103734
|
+
recoverySequence,
|
|
103735
|
+
fundingModel,
|
|
103736
|
+
network,
|
|
103737
|
+
prevOutScript,
|
|
103738
|
+
prevOutValue,
|
|
103739
|
+
signatures
|
|
103740
|
+
} = params;
|
|
103741
|
+
const inputIndex = params.inputIndex ?? 0;
|
|
103742
|
+
if (cohortKeys.length === 0) {
|
|
103743
|
+
throw new AggregationCohortError(
|
|
103744
|
+
"Cannot build fallback spend: no cohort keys.",
|
|
103745
|
+
"NO_COHORT_KEYS"
|
|
103746
|
+
);
|
|
103747
|
+
}
|
|
103748
|
+
const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
|
|
103749
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
103750
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
103751
|
+
recoveryKey,
|
|
103752
|
+
recoverySequence,
|
|
103753
|
+
cohortKeys,
|
|
103754
|
+
fallbackThreshold
|
|
103755
|
+
});
|
|
103756
|
+
const net = getNetwork(network);
|
|
103757
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
103758
|
+
if (!bytesEqual(payment.script, prevOutScript)) {
|
|
103759
|
+
throw new AggregationCohortError(
|
|
103760
|
+
"Reconstructed beacon output script does not match the spent prevout script.",
|
|
103761
|
+
"PREVOUT_SCRIPT_MISMATCH"
|
|
103762
|
+
);
|
|
103763
|
+
}
|
|
103764
|
+
const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
|
|
103765
|
+
const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
|
|
103766
|
+
const script = scriptVer.slice(0, scriptVer.length - 1);
|
|
103767
|
+
return bytesEqual(script, fallbackLeaf);
|
|
103768
|
+
});
|
|
103769
|
+
if (!leafEntry) {
|
|
103770
|
+
throw new AggregationCohortError(
|
|
103771
|
+
"Could not locate the fallback leaf in the reconstructed beacon output.",
|
|
103772
|
+
"FALLBACK_LEAF_NOT_FOUND"
|
|
103773
|
+
);
|
|
103774
|
+
}
|
|
103775
|
+
const cohortXOnly = sortKeys(cohortKeys).map((k) => k.slice(1));
|
|
103776
|
+
const tx = pendingTx;
|
|
103777
|
+
const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
|
|
103778
|
+
const leafHash = tapLeafHash2(fallbackLeaf);
|
|
103779
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
103780
|
+
for (const { pubKey, signature } of signatures) {
|
|
103781
|
+
if (pubKey.length !== 32 || signature.length !== 64) continue;
|
|
103782
|
+
const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
|
|
103783
|
+
if (!isCohortKey) continue;
|
|
103784
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103785
|
+
if (accepted.has(hex2)) continue;
|
|
103786
|
+
let ok = false;
|
|
103787
|
+
try {
|
|
103788
|
+
ok = schnorr2.verify(signature, sighash, pubKey);
|
|
103789
|
+
} catch {
|
|
103790
|
+
ok = false;
|
|
103791
|
+
}
|
|
103792
|
+
if (ok) accepted.set(hex2, signature);
|
|
103793
|
+
}
|
|
103794
|
+
if (accepted.size < fallbackThreshold) {
|
|
103795
|
+
throw new AggregationCohortError(
|
|
103796
|
+
`Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
|
|
103797
|
+
"NOT_ENOUGH_FALLBACK_SIGNATURES",
|
|
103798
|
+
{ have: accepted.size, need: fallbackThreshold }
|
|
103799
|
+
);
|
|
103800
|
+
}
|
|
103801
|
+
const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
|
|
103802
|
+
tx.updateInput(inputIndex, {
|
|
103803
|
+
tapLeafScript: [leafEntry],
|
|
103804
|
+
tapScriptSig: chosen.map((pubKey) => {
|
|
103805
|
+
const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
103806
|
+
return [{ pubKey, leafHash }, accepted.get(hex2)];
|
|
103807
|
+
})
|
|
103808
|
+
});
|
|
103809
|
+
tx.finalize();
|
|
103810
|
+
return tx;
|
|
103811
|
+
}
|
|
103812
|
+
|
|
103465
103813
|
// src/core/aggregation/messages/base.ts
|
|
103466
103814
|
init_shim();
|
|
103467
103815
|
var AGGREGATION_WIRE_VERSION = 1;
|
|
@@ -103501,12 +103849,15 @@ var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
|
|
|
103501
103849
|
var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
|
|
103502
103850
|
var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
|
|
103503
103851
|
var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
|
|
103852
|
+
var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
|
|
103504
103853
|
var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
|
|
103505
103854
|
var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
|
|
103506
103855
|
var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
|
|
103507
103856
|
var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
|
|
103508
103857
|
var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
|
|
103509
103858
|
var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
|
|
103859
|
+
var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
|
|
103860
|
+
var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
|
|
103510
103861
|
|
|
103511
103862
|
// src/core/aggregation/messages/factories.ts
|
|
103512
103863
|
init_shim();
|
|
@@ -103530,6 +103881,10 @@ function createSubmitUpdateMessage(fields) {
|
|
|
103530
103881
|
const { from: from8, to, ...body } = fields;
|
|
103531
103882
|
return new BaseMessage({ type: SUBMIT_UPDATE, from: from8, to, body });
|
|
103532
103883
|
}
|
|
103884
|
+
function createSubmitNonIncludedMessage(fields) {
|
|
103885
|
+
const { from: from8, to, ...body } = fields;
|
|
103886
|
+
return new BaseMessage({ type: SUBMIT_NONINCLUDED, from: from8, to, body });
|
|
103887
|
+
}
|
|
103533
103888
|
function createDistributeAggregatedDataMessage(fields) {
|
|
103534
103889
|
const { from: from8, to, ...body } = fields;
|
|
103535
103890
|
return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from: from8, to, body });
|
|
@@ -103554,6 +103909,14 @@ function createSignatureAuthorizationMessage(fields) {
|
|
|
103554
103909
|
const { from: from8, to, ...body } = fields;
|
|
103555
103910
|
return new BaseMessage({ type: SIGNATURE_AUTHORIZATION, from: from8, to, body });
|
|
103556
103911
|
}
|
|
103912
|
+
function createFallbackAuthorizationRequestMessage(fields) {
|
|
103913
|
+
const { from: from8, to, ...body } = fields;
|
|
103914
|
+
return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from: from8, to, body });
|
|
103915
|
+
}
|
|
103916
|
+
function createFallbackSignatureMessage(fields) {
|
|
103917
|
+
const { from: from8, to, ...body } = fields;
|
|
103918
|
+
return new BaseMessage({ type: FALLBACK_SIGNATURE, from: from8, to, body });
|
|
103919
|
+
}
|
|
103557
103920
|
|
|
103558
103921
|
// src/core/aggregation/phases.ts
|
|
103559
103922
|
init_shim();
|
|
@@ -103568,6 +103931,7 @@ var ServiceCohortPhase = /* @__PURE__ */ ((ServiceCohortPhase2) => {
|
|
|
103568
103931
|
ServiceCohortPhase2["SigningStarted"] = "SigningStarted";
|
|
103569
103932
|
ServiceCohortPhase2["NoncesCollected"] = "NoncesCollected";
|
|
103570
103933
|
ServiceCohortPhase2["AwaitingPartialSigs"] = "AwaitingPartialSigs";
|
|
103934
|
+
ServiceCohortPhase2["FallbackRequested"] = "FallbackRequested";
|
|
103571
103935
|
ServiceCohortPhase2["Complete"] = "Complete";
|
|
103572
103936
|
ServiceCohortPhase2["Failed"] = "Failed";
|
|
103573
103937
|
return ServiceCohortPhase2;
|
|
@@ -103577,11 +103941,13 @@ var ParticipantCohortPhase = /* @__PURE__ */ ((ParticipantCohortPhase2) => {
|
|
|
103577
103941
|
ParticipantCohortPhase2["OptedIn"] = "OptedIn";
|
|
103578
103942
|
ParticipantCohortPhase2["CohortReady"] = "CohortReady";
|
|
103579
103943
|
ParticipantCohortPhase2["UpdateSubmitted"] = "UpdateSubmitted";
|
|
103944
|
+
ParticipantCohortPhase2["NonIncluded"] = "NonIncluded";
|
|
103580
103945
|
ParticipantCohortPhase2["AwaitingValidation"] = "AwaitingValidation";
|
|
103581
103946
|
ParticipantCohortPhase2["ValidationSent"] = "ValidationSent";
|
|
103582
103947
|
ParticipantCohortPhase2["AwaitingSigning"] = "AwaitingSigning";
|
|
103583
103948
|
ParticipantCohortPhase2["NonceSent"] = "NonceSent";
|
|
103584
103949
|
ParticipantCohortPhase2["AwaitingPartialSig"] = "AwaitingPartialSig";
|
|
103950
|
+
ParticipantCohortPhase2["AwaitingFallbackSig"] = "AwaitingFallbackSig";
|
|
103585
103951
|
ParticipantCohortPhase2["Complete"] = "Complete";
|
|
103586
103952
|
ParticipantCohortPhase2["Failed"] = "Failed";
|
|
103587
103953
|
return ParticipantCohortPhase2;
|
|
@@ -103609,11 +103975,11 @@ var BeaconSigningSession = class {
|
|
|
103609
103975
|
prevOutScripts;
|
|
103610
103976
|
/** Previous output values for Taproot sighash computation. */
|
|
103611
103977
|
prevOutValues;
|
|
103612
|
-
/** Map of participant publicKey-hex
|
|
103978
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
103613
103979
|
nonceContributions = /* @__PURE__ */ new Map();
|
|
103614
103980
|
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
103615
103981
|
aggregatedNonce;
|
|
103616
|
-
/** Map of participant DID
|
|
103982
|
+
/** Map of participant DID to partial signature. */
|
|
103617
103983
|
partialSignatures = /* @__PURE__ */ new Map();
|
|
103618
103984
|
/** Final 64-byte Schnorr signature. */
|
|
103619
103985
|
signature;
|
|
@@ -103796,7 +104162,7 @@ var BeaconSigningSession = class {
|
|
|
103796
104162
|
throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
|
|
103797
104163
|
}
|
|
103798
104164
|
if (!this.#secretNonce) {
|
|
103799
|
-
throw new SigningSessionError("Secret nonce not available
|
|
104165
|
+
throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
|
|
103800
104166
|
}
|
|
103801
104167
|
const session = new Session(
|
|
103802
104168
|
this.aggregatedNonce,
|
|
@@ -103866,6 +104232,9 @@ var AggregationService = class {
|
|
|
103866
104232
|
case SUBMIT_UPDATE:
|
|
103867
104233
|
this.#handleSubmitUpdate(message2);
|
|
103868
104234
|
break;
|
|
104235
|
+
case SUBMIT_NONINCLUDED:
|
|
104236
|
+
this.#handleSubmitNonInclusion(message2);
|
|
104237
|
+
break;
|
|
103869
104238
|
case VALIDATION_ACK:
|
|
103870
104239
|
this.#handleValidationAck(message2);
|
|
103871
104240
|
break;
|
|
@@ -103875,6 +104244,9 @@ var AggregationService = class {
|
|
|
103875
104244
|
case SIGNATURE_AUTHORIZATION:
|
|
103876
104245
|
this.#handleSignatureAuthorization(message2);
|
|
103877
104246
|
break;
|
|
104247
|
+
case FALLBACK_SIGNATURE:
|
|
104248
|
+
this.#handleFallbackSignature(message2);
|
|
104249
|
+
break;
|
|
103878
104250
|
default:
|
|
103879
104251
|
break;
|
|
103880
104252
|
}
|
|
@@ -103893,7 +104265,7 @@ var AggregationService = class {
|
|
|
103893
104265
|
}
|
|
103894
104266
|
/**
|
|
103895
104267
|
* Create a new cohort with the given config. Returns the cohort ID.
|
|
103896
|
-
* Cohort starts in `Created` phase
|
|
104268
|
+
* Cohort starts in `Created` phase - call `advertise()` to broadcast.
|
|
103897
104269
|
*/
|
|
103898
104270
|
createCohort(config) {
|
|
103899
104271
|
const problems = validateCohortConditions(config);
|
|
@@ -103908,7 +104280,11 @@ var AggregationService = class {
|
|
|
103908
104280
|
serviceDid: this.did,
|
|
103909
104281
|
minParticipants: config.minParticipants,
|
|
103910
104282
|
network: config.network,
|
|
103911
|
-
beaconType: config.beaconType
|
|
104283
|
+
beaconType: config.beaconType,
|
|
104284
|
+
recoveryKey: hexToBytes(config.recoveryKey),
|
|
104285
|
+
recoverySequence: config.recoverySequence,
|
|
104286
|
+
fundingModel: config.fundingModel,
|
|
104287
|
+
fallbackThreshold: config.fallbackThreshold
|
|
103912
104288
|
});
|
|
103913
104289
|
this.#cohortStates.set(cohort.id, {
|
|
103914
104290
|
phase: "Created" /* Created */,
|
|
@@ -104109,11 +104485,64 @@ var AggregationService = class {
|
|
|
104109
104485
|
});
|
|
104110
104486
|
return;
|
|
104111
104487
|
}
|
|
104488
|
+
if (state.cohort.nonIncluded.has(message2.from)) {
|
|
104489
|
+
state.rejections.push({
|
|
104490
|
+
from: message2.from,
|
|
104491
|
+
code: "UPDATE_MALFORMED",
|
|
104492
|
+
reason: "Participant already declined this round; cannot also submit an update"
|
|
104493
|
+
});
|
|
104494
|
+
return;
|
|
104495
|
+
}
|
|
104496
|
+
if (state.cohort.pendingUpdates.has(message2.from)) {
|
|
104497
|
+
state.rejections.push({
|
|
104498
|
+
from: message2.from,
|
|
104499
|
+
code: "UPDATE_MALFORMED",
|
|
104500
|
+
reason: "Participant already submitted an update this round; cannot resubmit"
|
|
104501
|
+
});
|
|
104502
|
+
return;
|
|
104503
|
+
}
|
|
104112
104504
|
state.cohort.addUpdate(message2.from, signedUpdate);
|
|
104113
104505
|
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104114
104506
|
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104115
104507
|
}
|
|
104116
|
-
if (state.cohort.
|
|
104508
|
+
if (state.cohort.hasAllResponses()) {
|
|
104509
|
+
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104510
|
+
}
|
|
104511
|
+
}
|
|
104512
|
+
/**
|
|
104513
|
+
* Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
|
|
104514
|
+
* update this round (cooperative non-inclusion). Membership is proven by the
|
|
104515
|
+
* signed transport envelope, so the body carries only the cohortId. The member
|
|
104516
|
+
* stays in the cohort and still signs; it is absent from the CAS map and
|
|
104517
|
+
* carries a non-inclusion leaf in the SMT.
|
|
104518
|
+
*/
|
|
104519
|
+
#handleSubmitNonInclusion(message2) {
|
|
104520
|
+
const cohortId = message2.body?.cohortId;
|
|
104521
|
+
if (!cohortId) return;
|
|
104522
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104523
|
+
if (!state) return;
|
|
104524
|
+
if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
|
|
104525
|
+
if (!state.cohort.participants.includes(message2.from)) {
|
|
104526
|
+
state.rejections.push({
|
|
104527
|
+
from: message2.from,
|
|
104528
|
+
code: "UPDATE_MALFORMED",
|
|
104529
|
+
reason: "Sender is not a member of this cohort"
|
|
104530
|
+
});
|
|
104531
|
+
return;
|
|
104532
|
+
}
|
|
104533
|
+
if (state.cohort.pendingUpdates.has(message2.from) || state.cohort.nonIncluded.has(message2.from)) {
|
|
104534
|
+
state.rejections.push({
|
|
104535
|
+
from: message2.from,
|
|
104536
|
+
code: "UPDATE_MALFORMED",
|
|
104537
|
+
reason: "Participant already responded this round"
|
|
104538
|
+
});
|
|
104539
|
+
return;
|
|
104540
|
+
}
|
|
104541
|
+
state.cohort.addNonInclusion(message2.from);
|
|
104542
|
+
if (state.phase === "CohortSet" /* CohortSet */) {
|
|
104543
|
+
state.phase = "CollectingUpdates" /* CollectingUpdates */;
|
|
104544
|
+
}
|
|
104545
|
+
if (state.cohort.hasAllResponses()) {
|
|
104117
104546
|
state.phase = "UpdatesCollected" /* UpdatesCollected */;
|
|
104118
104547
|
}
|
|
104119
104548
|
}
|
|
@@ -104223,7 +104652,7 @@ var AggregationService = class {
|
|
|
104223
104652
|
}
|
|
104224
104653
|
/**
|
|
104225
104654
|
* Start a signing session by creating auth requests for all participants.
|
|
104226
|
-
* The caller provides the transaction data
|
|
104655
|
+
* The caller provides the transaction data - typically built via
|
|
104227
104656
|
* `buildBeaconTransaction()` against a Bitcoin connection.
|
|
104228
104657
|
*/
|
|
104229
104658
|
startSigning(cohortId, txData) {
|
|
@@ -104330,11 +104759,144 @@ var AggregationService = class {
|
|
|
104330
104759
|
state.result = {
|
|
104331
104760
|
cohortId,
|
|
104332
104761
|
signature,
|
|
104333
|
-
signedTx: state.signingSession.pendingTx
|
|
104762
|
+
signedTx: state.signingSession.pendingTx,
|
|
104763
|
+
path: "key-path"
|
|
104334
104764
|
};
|
|
104335
104765
|
state.phase = "Complete" /* Complete */;
|
|
104336
104766
|
}
|
|
104337
104767
|
}
|
|
104768
|
+
/**
|
|
104769
|
+
* Abandon the optimistic n-of-n key path and ask members to authorize the
|
|
104770
|
+
* k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
|
|
104771
|
+
* liveness, ADR 042). Reuses the in-flight signing session's transaction and
|
|
104772
|
+
* spent output, so the announcement and its outputs are unchanged: only the
|
|
104773
|
+
* witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
|
|
104774
|
+
* participant.
|
|
104775
|
+
*
|
|
104776
|
+
* Callable once optimistic signing has started (the session and its tx exist)
|
|
104777
|
+
* and before it completes. A cohort can take exactly one of the two paths: the
|
|
104778
|
+
* caller (runner) must commit to fallback and stop driving the optimistic path.
|
|
104779
|
+
*/
|
|
104780
|
+
startFallbackSigning(cohortId) {
|
|
104781
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104782
|
+
if (!state) {
|
|
104783
|
+
throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
|
|
104784
|
+
}
|
|
104785
|
+
if (!state.signingSession) {
|
|
104786
|
+
throw new AggregationServiceError(
|
|
104787
|
+
`Cannot start fallback for cohort ${cohortId}: no signing session.`,
|
|
104788
|
+
"NO_SIGNING_SESSION",
|
|
104789
|
+
{ cohortId }
|
|
104790
|
+
);
|
|
104791
|
+
}
|
|
104792
|
+
const signingPhases = [
|
|
104793
|
+
"SigningStarted" /* SigningStarted */,
|
|
104794
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
104795
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
104796
|
+
];
|
|
104797
|
+
if (!signingPhases.includes(state.phase)) {
|
|
104798
|
+
throw new AggregationServiceError(
|
|
104799
|
+
`Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
|
|
104800
|
+
"INVALID_PHASE",
|
|
104801
|
+
{ cohortId, phase: state.phase }
|
|
104802
|
+
);
|
|
104803
|
+
}
|
|
104804
|
+
const session = state.signingSession;
|
|
104805
|
+
const prevOutScript = session.prevOutScripts[0];
|
|
104806
|
+
const prevOutValue = session.prevOutValues[0];
|
|
104807
|
+
if (!prevOutScript || prevOutValue === void 0) {
|
|
104808
|
+
throw new AggregationServiceError(
|
|
104809
|
+
`Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
|
|
104810
|
+
"MISSING_PREV_OUT",
|
|
104811
|
+
{ cohortId }
|
|
104812
|
+
);
|
|
104813
|
+
}
|
|
104814
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
104815
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
104816
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
104817
|
+
});
|
|
104818
|
+
state.fallbackSignatures = /* @__PURE__ */ new Map();
|
|
104819
|
+
state.phase = "FallbackRequested" /* FallbackRequested */;
|
|
104820
|
+
const messages2 = [];
|
|
104821
|
+
for (const participantDid of state.cohort.participants) {
|
|
104822
|
+
messages2.push(createFallbackAuthorizationRequestMessage({
|
|
104823
|
+
from: this.did,
|
|
104824
|
+
to: participantDid,
|
|
104825
|
+
cohortId,
|
|
104826
|
+
sessionId: session.id,
|
|
104827
|
+
pendingTx: session.pendingTx.hex,
|
|
104828
|
+
prevOutScriptHex: bytesToHex(prevOutScript),
|
|
104829
|
+
prevOutValue: prevOutValue.toString(),
|
|
104830
|
+
fallbackLeafScriptHex: bytesToHex(fallbackLeaf)
|
|
104831
|
+
}));
|
|
104832
|
+
}
|
|
104833
|
+
return messages2;
|
|
104834
|
+
}
|
|
104835
|
+
/**
|
|
104836
|
+
* Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
|
|
104837
|
+
* signature over the fallback script-path sighash. The signature is
|
|
104838
|
+
* authenticated to the sender (its `signerPk` must be the sender's own cohort
|
|
104839
|
+
* key and the signature must verify against the sighash) and collected. Once k
|
|
104840
|
+
* valid signatures are in, the k-of-n fallback spend is assembled and the
|
|
104841
|
+
* cohort completes via the script path.
|
|
104842
|
+
*/
|
|
104843
|
+
#handleFallbackSignature(message2) {
|
|
104844
|
+
const cohortId = message2.body?.cohortId;
|
|
104845
|
+
if (!cohortId) return;
|
|
104846
|
+
const state = this.#cohortStates.get(cohortId);
|
|
104847
|
+
if (!state || !state.signingSession || !state.fallbackSignatures) return;
|
|
104848
|
+
if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
|
|
104849
|
+
const sessionId = message2.body?.sessionId;
|
|
104850
|
+
if (sessionId !== state.signingSession.id) return;
|
|
104851
|
+
const signerPk = message2.body?.signerPk;
|
|
104852
|
+
const fallbackSignature = message2.body?.fallbackSignature;
|
|
104853
|
+
if (!signerPk || !fallbackSignature) return;
|
|
104854
|
+
const prevOutScript = state.signingSession.prevOutScripts[0];
|
|
104855
|
+
const prevOutValue = state.signingSession.prevOutValues[0];
|
|
104856
|
+
if (!prevOutScript || prevOutValue === void 0) return;
|
|
104857
|
+
const memberKey = state.cohort.participantKeys.get(message2.from);
|
|
104858
|
+
if (!memberKey) {
|
|
104859
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
|
|
104860
|
+
return;
|
|
104861
|
+
}
|
|
104862
|
+
const memberXOnly = memberKey.slice(1);
|
|
104863
|
+
if (signerPk.length !== 32 || !memberXOnly.every((b, i4) => b === signerPk[i4])) {
|
|
104864
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
|
|
104865
|
+
return;
|
|
104866
|
+
}
|
|
104867
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
104868
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
104869
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
104870
|
+
});
|
|
104871
|
+
const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
104872
|
+
let valid = false;
|
|
104873
|
+
try {
|
|
104874
|
+
valid = fallbackSignature.length === 64 && schnorr2.verify(fallbackSignature, sighash, signerPk);
|
|
104875
|
+
} catch {
|
|
104876
|
+
valid = false;
|
|
104877
|
+
}
|
|
104878
|
+
if (!valid) {
|
|
104879
|
+
state.rejections.push({ from: message2.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
|
|
104880
|
+
return;
|
|
104881
|
+
}
|
|
104882
|
+
state.fallbackSignatures.set(message2.from, { pubKey: signerPk, signature: fallbackSignature });
|
|
104883
|
+
if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
|
|
104884
|
+
const signedTx = buildFallbackSpend({
|
|
104885
|
+
pendingTx: state.signingSession.pendingTx,
|
|
104886
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
104887
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold,
|
|
104888
|
+
recoveryKey: state.cohort.recoveryKey,
|
|
104889
|
+
recoverySequence: state.cohort.recoverySequence,
|
|
104890
|
+
fundingModel: state.cohort.fundingModel,
|
|
104891
|
+
network: state.cohort.network,
|
|
104892
|
+
prevOutScript,
|
|
104893
|
+
prevOutValue,
|
|
104894
|
+
signatures: [...state.fallbackSignatures.values()]
|
|
104895
|
+
});
|
|
104896
|
+
state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
|
|
104897
|
+
state.phase = "Complete" /* Complete */;
|
|
104898
|
+
}
|
|
104899
|
+
}
|
|
104338
104900
|
getResult(cohortId) {
|
|
104339
104901
|
return this.#cohortStates.get(cohortId)?.result;
|
|
104340
104902
|
}
|
|
@@ -104366,6 +104928,7 @@ var AggregationService = class {
|
|
|
104366
104928
|
|
|
104367
104929
|
// src/core/aggregation/participant.ts
|
|
104368
104930
|
init_shim();
|
|
104931
|
+
init_secp256k1();
|
|
104369
104932
|
init_utils();
|
|
104370
104933
|
|
|
104371
104934
|
// src/core/aggregation/messages/bodies.ts
|
|
@@ -104379,6 +104942,14 @@ var optIntMin = (b, k, min) => {
|
|
|
104379
104942
|
const v = b ? b[k] : void 0;
|
|
104380
104943
|
return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
104381
104944
|
};
|
|
104945
|
+
var intInRange = (b, k, min, max) => {
|
|
104946
|
+
const v = b ? b[k] : void 0;
|
|
104947
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
104948
|
+
};
|
|
104949
|
+
var optStrOneOf = (b, k, allowed) => {
|
|
104950
|
+
const v = b ? b[k] : void 0;
|
|
104951
|
+
return v === void 0 || typeof v === "string" && allowed.includes(v);
|
|
104952
|
+
};
|
|
104382
104953
|
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
104383
104954
|
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
104384
104955
|
var hasBytesArray = (b, k) => {
|
|
@@ -104386,7 +104957,7 @@ var hasBytesArray = (b, k) => {
|
|
|
104386
104957
|
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
104387
104958
|
};
|
|
104388
104959
|
function isCohortAdvertMessage(m2) {
|
|
104389
|
-
return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasBytes(m2.body, "communicationPk");
|
|
104960
|
+
return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasStr(m2.body, "recoveryKey") && intInRange(m2.body, "recoverySequence", 1, MAX_RECOVERY_SEQUENCE) && optStrOneOf(m2.body, "fundingModel", KNOWN_FUNDING_MODELS) && optIntMin(m2.body, "fallbackThreshold", 1) && hasBytes(m2.body, "communicationPk");
|
|
104390
104961
|
}
|
|
104391
104962
|
function isCohortOptInMessage(m2) {
|
|
104392
104963
|
return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
|
|
@@ -104400,6 +104971,9 @@ function isCohortReadyMessage(m2) {
|
|
|
104400
104971
|
function isSubmitUpdateMessage(m2) {
|
|
104401
104972
|
return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
|
|
104402
104973
|
}
|
|
104974
|
+
function isSubmitNonIncludedMessage(m2) {
|
|
104975
|
+
return m2.type === SUBMIT_NONINCLUDED && hasStr(m2.body, "cohortId");
|
|
104976
|
+
}
|
|
104403
104977
|
function isDistributeAggregatedDataMessage(m2) {
|
|
104404
104978
|
return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
|
|
104405
104979
|
}
|
|
@@ -104418,8 +104992,38 @@ function isAggregatedNonceMessage(m2) {
|
|
|
104418
104992
|
function isSignatureAuthorizationMessage(m2) {
|
|
104419
104993
|
return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
|
|
104420
104994
|
}
|
|
104995
|
+
function isFallbackAuthorizationRequestMessage(m2) {
|
|
104996
|
+
return m2.type === FALLBACK_AUTHORIZATION_REQUEST && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasStr(m2.body, "pendingTx") && hasStr(m2.body, "prevOutScriptHex") && hasStr(m2.body, "prevOutValue") && hasStr(m2.body, "fallbackLeafScriptHex");
|
|
104997
|
+
}
|
|
104998
|
+
function isFallbackSignatureMessage(m2) {
|
|
104999
|
+
return m2.type === FALLBACK_SIGNATURE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "signerPk") && hasBytes(m2.body, "fallbackSignature");
|
|
105000
|
+
}
|
|
104421
105001
|
|
|
104422
105002
|
// src/core/aggregation/participant.ts
|
|
105003
|
+
function txEmbedsSignal(tx, signalHex) {
|
|
105004
|
+
let expected;
|
|
105005
|
+
try {
|
|
105006
|
+
expected = hexToBytes(signalHex);
|
|
105007
|
+
} catch {
|
|
105008
|
+
return false;
|
|
105009
|
+
}
|
|
105010
|
+
if (expected.length === 0) return false;
|
|
105011
|
+
for (let i4 = 0; i4 < tx.outputsLength; i4++) {
|
|
105012
|
+
const script = tx.getOutput(i4)?.script;
|
|
105013
|
+
if (!script) continue;
|
|
105014
|
+
let decoded;
|
|
105015
|
+
try {
|
|
105016
|
+
decoded = Script.decode(script);
|
|
105017
|
+
} catch {
|
|
105018
|
+
continue;
|
|
105019
|
+
}
|
|
105020
|
+
if (decoded.length === 2 && decoded[0] === "RETURN" && decoded[1] instanceof Uint8Array) {
|
|
105021
|
+
const payload = decoded[1];
|
|
105022
|
+
if (payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
|
|
105023
|
+
}
|
|
105024
|
+
}
|
|
105025
|
+
return false;
|
|
105026
|
+
}
|
|
104423
105027
|
var AggregationParticipant = class {
|
|
104424
105028
|
did;
|
|
104425
105029
|
/** MuSig2 signing capability. The raw secret never lives as a field here. */
|
|
@@ -104436,7 +105040,7 @@ var AggregationParticipant = class {
|
|
|
104436
105040
|
}
|
|
104437
105041
|
/**
|
|
104438
105042
|
* Process an incoming message. Updates internal state but never produces
|
|
104439
|
-
* outgoing messages
|
|
105043
|
+
* outgoing messages: those come exclusively from action methods.
|
|
104440
105044
|
*/
|
|
104441
105045
|
receive(message2) {
|
|
104442
105046
|
if (message2.version === void 0 || message2.version !== AGGREGATION_WIRE_VERSION) {
|
|
@@ -104462,6 +105066,9 @@ var AggregationParticipant = class {
|
|
|
104462
105066
|
case AGGREGATED_NONCE:
|
|
104463
105067
|
this.#handleAggregatedNonce(message2);
|
|
104464
105068
|
break;
|
|
105069
|
+
case FALLBACK_AUTHORIZATION_REQUEST:
|
|
105070
|
+
this.#handleFallbackAuthorizationRequest(message2);
|
|
105071
|
+
break;
|
|
104465
105072
|
default:
|
|
104466
105073
|
break;
|
|
104467
105074
|
}
|
|
@@ -104507,12 +105114,17 @@ var AggregationParticipant = class {
|
|
|
104507
105114
|
{ cohortId, phase: state?.phase }
|
|
104508
105115
|
);
|
|
104509
105116
|
}
|
|
105117
|
+
const advert = state.advert;
|
|
104510
105118
|
const cohort = new AggregationCohort({
|
|
104511
105119
|
id: cohortId,
|
|
104512
105120
|
serviceDid: state.serviceDid,
|
|
104513
|
-
minParticipants:
|
|
104514
|
-
network:
|
|
104515
|
-
beaconType:
|
|
105121
|
+
minParticipants: advert.minParticipants,
|
|
105122
|
+
network: advert.network,
|
|
105123
|
+
beaconType: advert.beaconType,
|
|
105124
|
+
recoveryKey: advert.recoveryKey ? hexToBytes(advert.recoveryKey) : void 0,
|
|
105125
|
+
recoverySequence: advert.recoverySequence,
|
|
105126
|
+
fundingModel: advert.fundingModel,
|
|
105127
|
+
fallbackThreshold: advert.fallbackThreshold
|
|
104516
105128
|
});
|
|
104517
105129
|
state.cohort = cohort;
|
|
104518
105130
|
state.phase = "OptedIn" /* OptedIn */;
|
|
@@ -104527,7 +105139,7 @@ var AggregationParticipant = class {
|
|
|
104527
105139
|
}
|
|
104528
105140
|
#handleOptInAccept(message2) {
|
|
104529
105141
|
}
|
|
104530
|
-
/** Cohorts that have been finalized
|
|
105142
|
+
/** Cohorts that have been finalized: beacon address available. */
|
|
104531
105143
|
get joinedCohorts() {
|
|
104532
105144
|
const map3 = /* @__PURE__ */ new Map();
|
|
104533
105145
|
for (const [id, state] of this.#cohortStates) {
|
|
@@ -104570,6 +105182,7 @@ var AggregationParticipant = class {
|
|
|
104570
105182
|
);
|
|
104571
105183
|
}
|
|
104572
105184
|
state.submittedUpdate = signedUpdate;
|
|
105185
|
+
state.included = true;
|
|
104573
105186
|
state.phase = "UpdateSubmitted" /* UpdateSubmitted */;
|
|
104574
105187
|
const message2 = createSubmitUpdateMessage({
|
|
104575
105188
|
from: this.did,
|
|
@@ -104579,6 +105192,30 @@ var AggregationParticipant = class {
|
|
|
104579
105192
|
});
|
|
104580
105193
|
return [message2];
|
|
104581
105194
|
}
|
|
105195
|
+
/**
|
|
105196
|
+
* User action: decline to submit an update this round (cooperative
|
|
105197
|
+
* non-inclusion). The member stays in the cohort and still signs; it will be
|
|
105198
|
+
* absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
|
|
105199
|
+
* SMT. Returns the SUBMIT_NONINCLUDED message to send.
|
|
105200
|
+
*/
|
|
105201
|
+
declineUpdate(cohortId) {
|
|
105202
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105203
|
+
if (!state || state.phase !== "CohortReady" /* CohortReady */) {
|
|
105204
|
+
throw new AggregationParticipantError(
|
|
105205
|
+
`Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
|
|
105206
|
+
"INVALID_PHASE",
|
|
105207
|
+
{ cohortId, phase: state?.phase }
|
|
105208
|
+
);
|
|
105209
|
+
}
|
|
105210
|
+
state.included = false;
|
|
105211
|
+
state.phase = "NonIncluded" /* NonIncluded */;
|
|
105212
|
+
const message2 = createSubmitNonIncludedMessage({
|
|
105213
|
+
from: this.did,
|
|
105214
|
+
to: state.serviceDid,
|
|
105215
|
+
cohortId
|
|
105216
|
+
});
|
|
105217
|
+
return [message2];
|
|
105218
|
+
}
|
|
104582
105219
|
/** Aggregated data awaiting user validation. */
|
|
104583
105220
|
get pendingValidations() {
|
|
104584
105221
|
const map3 = /* @__PURE__ */ new Map();
|
|
@@ -104592,9 +105229,9 @@ var AggregationParticipant = class {
|
|
|
104592
105229
|
/**
|
|
104593
105230
|
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
104594
105231
|
* Unlike {@link pendingValidations} (which lists only cohorts still awaiting
|
|
104595
|
-
* the validate decision), this returns the stored validation
|
|
104596
|
-
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
|
|
104597
|
-
*
|
|
105232
|
+
* the validate decision), this returns the stored validation, including the
|
|
105233
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
|
|
105234
|
+
* so it is still readable once the cohort reaches Complete. Returns
|
|
104598
105235
|
* undefined before aggregated data has been received.
|
|
104599
105236
|
*/
|
|
104600
105237
|
getValidation(cohortId) {
|
|
@@ -104604,18 +105241,20 @@ var AggregationParticipant = class {
|
|
|
104604
105241
|
const cohortId = message2.body?.cohortId;
|
|
104605
105242
|
if (!cohortId) return;
|
|
104606
105243
|
const state = this.#cohortStates.get(cohortId);
|
|
104607
|
-
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
|
|
104608
|
-
|
|
105244
|
+
if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */ && state.phase !== "NonIncluded" /* NonIncluded */) return;
|
|
105245
|
+
const declined = state.included === false;
|
|
105246
|
+
if (!declined && !state.submittedUpdate) return;
|
|
104609
105247
|
const beaconType = message2.body?.beaconType;
|
|
104610
105248
|
if (!beaconType) return;
|
|
104611
105249
|
const strategy = getBeaconStrategy(beaconType);
|
|
104612
105250
|
if (!strategy) return;
|
|
104613
105251
|
const signalBytesHex = message2.body?.signalBytesHex ?? "";
|
|
104614
|
-
const expectedHash = canonicalHash(state.submittedUpdate);
|
|
105252
|
+
const expectedHash = declined ? "" : canonicalHash(state.submittedUpdate);
|
|
104615
105253
|
const result = strategy.validateParticipantView({
|
|
104616
105254
|
participantDid: this.did,
|
|
104617
|
-
|
|
104618
|
-
|
|
105255
|
+
included: !declined,
|
|
105256
|
+
submittedUpdate: declined ? void 0 : state.submittedUpdate,
|
|
105257
|
+
expectedHash: declined ? void 0 : expectedHash,
|
|
104619
105258
|
body: message2.body
|
|
104620
105259
|
});
|
|
104621
105260
|
state.validation = {
|
|
@@ -104625,7 +105264,8 @@ var AggregationParticipant = class {
|
|
|
104625
105264
|
expectedHash,
|
|
104626
105265
|
matches: result.matches,
|
|
104627
105266
|
casAnnouncement: result.casAnnouncement,
|
|
104628
|
-
smtProof: result.smtProof
|
|
105267
|
+
smtProof: result.smtProof,
|
|
105268
|
+
included: !declined
|
|
104629
105269
|
};
|
|
104630
105270
|
state.phase = "AwaitingValidation" /* AwaitingValidation */;
|
|
104631
105271
|
}
|
|
@@ -104699,6 +105339,31 @@ var AggregationParticipant = class {
|
|
|
104699
105339
|
};
|
|
104700
105340
|
state.phase = "AwaitingSigning" /* AwaitingSigning */;
|
|
104701
105341
|
}
|
|
105342
|
+
/**
|
|
105343
|
+
* Bind a signing approval to the announcement the member validated: a beacon
|
|
105344
|
+
* transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
|
|
105345
|
+
* the aggregated data was distributed. Both the optimistic nonce approval and
|
|
105346
|
+
* the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
|
|
105347
|
+
* while the coordinator drives output selection, so without this check a
|
|
105348
|
+
* coordinator could anchor a different signal under the member's signature.
|
|
105349
|
+
*/
|
|
105350
|
+
#assertTxAnchorsValidatedSignal(cohortId, state, tx) {
|
|
105351
|
+
const signalHex = state.validation?.signalBytesHex;
|
|
105352
|
+
if (!signalHex) {
|
|
105353
|
+
throw new AggregationParticipantError(
|
|
105354
|
+
`Cohort ${cohortId} has no validated signal to bind the signature to.`,
|
|
105355
|
+
"MISSING_STATE",
|
|
105356
|
+
{ cohortId }
|
|
105357
|
+
);
|
|
105358
|
+
}
|
|
105359
|
+
if (!txEmbedsSignal(tx, signalHex)) {
|
|
105360
|
+
throw new AggregationParticipantError(
|
|
105361
|
+
`Transaction for cohort ${cohortId} does not anchor the validated signal.`,
|
|
105362
|
+
"SIGNAL_MISMATCH",
|
|
105363
|
+
{ cohortId }
|
|
105364
|
+
);
|
|
105365
|
+
}
|
|
105366
|
+
}
|
|
104702
105367
|
/**
|
|
104703
105368
|
* User action: approve signing and generate nonce contribution.
|
|
104704
105369
|
*/
|
|
@@ -104718,7 +105383,8 @@ var AggregationParticipant = class {
|
|
|
104718
105383
|
{ cohortId }
|
|
104719
105384
|
);
|
|
104720
105385
|
}
|
|
104721
|
-
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
|
|
105386
|
+
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
|
|
105387
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
104722
105388
|
const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
|
|
104723
105389
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
104724
105390
|
const session = new BeaconSigningSession({
|
|
@@ -104754,7 +105420,7 @@ var AggregationParticipant = class {
|
|
|
104754
105420
|
}
|
|
104755
105421
|
/**
|
|
104756
105422
|
* User action: generate and return the partial signature.
|
|
104757
|
-
* In most UIs this is automatic after AwaitingPartialSig
|
|
105423
|
+
* In most UIs this is automatic after AwaitingPartialSig, but exposing it
|
|
104758
105424
|
* as an explicit action lets the client UI confirm before signing if desired.
|
|
104759
105425
|
*/
|
|
104760
105426
|
generatePartialSignature(cohortId) {
|
|
@@ -104786,6 +105452,83 @@ var AggregationParticipant = class {
|
|
|
104786
105452
|
partialSignature: partialSig
|
|
104787
105453
|
})];
|
|
104788
105454
|
}
|
|
105455
|
+
/** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
|
|
105456
|
+
get pendingFallbackRequests() {
|
|
105457
|
+
const map3 = /* @__PURE__ */ new Map();
|
|
105458
|
+
for (const [id, state] of this.#cohortStates) {
|
|
105459
|
+
if (state.phase === "AwaitingFallbackSig" /* AwaitingFallbackSig */ && state.fallbackRequest) {
|
|
105460
|
+
map3.set(id, state.fallbackRequest);
|
|
105461
|
+
}
|
|
105462
|
+
}
|
|
105463
|
+
return map3;
|
|
105464
|
+
}
|
|
105465
|
+
#handleFallbackAuthorizationRequest(message2) {
|
|
105466
|
+
const cohortId = message2.body?.cohortId;
|
|
105467
|
+
if (!cohortId) return;
|
|
105468
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105469
|
+
if (!state || !state.cohort) return;
|
|
105470
|
+
const acceptFrom = [
|
|
105471
|
+
"ValidationSent" /* ValidationSent */,
|
|
105472
|
+
"AwaitingSigning" /* AwaitingSigning */,
|
|
105473
|
+
"NonceSent" /* NonceSent */,
|
|
105474
|
+
"AwaitingPartialSig" /* AwaitingPartialSig */,
|
|
105475
|
+
"Complete" /* Complete */
|
|
105476
|
+
];
|
|
105477
|
+
if (!acceptFrom.includes(state.phase)) return;
|
|
105478
|
+
const sessionId = message2.body?.sessionId;
|
|
105479
|
+
const pendingTxHex = message2.body?.pendingTx;
|
|
105480
|
+
const prevOutScriptHex = message2.body?.prevOutScriptHex;
|
|
105481
|
+
const prevOutValue = message2.body?.prevOutValue;
|
|
105482
|
+
const fallbackLeafScriptHex = message2.body?.fallbackLeafScriptHex;
|
|
105483
|
+
if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
|
|
105484
|
+
state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
|
|
105485
|
+
state.signingSession?.clearSecrets();
|
|
105486
|
+
state.phase = "AwaitingFallbackSig" /* AwaitingFallbackSig */;
|
|
105487
|
+
}
|
|
105488
|
+
/**
|
|
105489
|
+
* User action: authorize the fallback spend. Recomputes the k-of-n fallback
|
|
105490
|
+
* leaf from the member's OWN cohort state (not the service-provided script),
|
|
105491
|
+
* computes the BIP-341 script-path sighash over the requested transaction, and
|
|
105492
|
+
* returns a standalone BIP-340 signature (no nonce round). The member completes
|
|
105493
|
+
* once it has contributed; the service needs only k of these.
|
|
105494
|
+
*/
|
|
105495
|
+
approveFallback(cohortId) {
|
|
105496
|
+
const state = this.#cohortStates.get(cohortId);
|
|
105497
|
+
if (!state || state.phase !== "AwaitingFallbackSig" /* AwaitingFallbackSig */) {
|
|
105498
|
+
throw new AggregationParticipantError(
|
|
105499
|
+
`Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
|
|
105500
|
+
"INVALID_PHASE",
|
|
105501
|
+
{ cohortId, phase: state?.phase }
|
|
105502
|
+
);
|
|
105503
|
+
}
|
|
105504
|
+
if (!state.fallbackRequest || !state.cohort) {
|
|
105505
|
+
throw new AggregationParticipantError(
|
|
105506
|
+
`Cohort ${cohortId} missing fallback request or cohort state.`,
|
|
105507
|
+
"MISSING_STATE",
|
|
105508
|
+
{ cohortId }
|
|
105509
|
+
);
|
|
105510
|
+
}
|
|
105511
|
+
const req = state.fallbackRequest;
|
|
105512
|
+
const tx = Transaction.fromRaw(hexToBytes(req.pendingTxHex), { allowUnknownOutputs: true });
|
|
105513
|
+
const prevOutScript = hexToBytes(req.prevOutScriptHex);
|
|
105514
|
+
const prevOutValue = BigInt(req.prevOutValue);
|
|
105515
|
+
this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
|
|
105516
|
+
const fallbackLeaf = buildFallbackLeaf({
|
|
105517
|
+
cohortKeys: state.cohort.cohortKeys,
|
|
105518
|
+
fallbackThreshold: state.cohort.effectiveFallbackThreshold
|
|
105519
|
+
});
|
|
105520
|
+
const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
|
|
105521
|
+
const signature = this.#signer.withSecret((secretKey) => schnorr2.sign(sighash, secretKey));
|
|
105522
|
+
state.phase = "Complete" /* Complete */;
|
|
105523
|
+
return [createFallbackSignatureMessage({
|
|
105524
|
+
from: this.did,
|
|
105525
|
+
to: state.serviceDid,
|
|
105526
|
+
cohortId,
|
|
105527
|
+
sessionId: req.sessionId,
|
|
105528
|
+
signerPk: this.publicKey.slice(1),
|
|
105529
|
+
fallbackSignature: signature
|
|
105530
|
+
})];
|
|
105531
|
+
}
|
|
104789
105532
|
getCohortPhase(cohortId) {
|
|
104790
105533
|
return this.#cohortStates.get(cohortId)?.phase;
|
|
104791
105534
|
}
|
|
@@ -104822,6 +105565,85 @@ var KeyPairAggregationSigner = class {
|
|
|
104822
105565
|
}
|
|
104823
105566
|
};
|
|
104824
105567
|
|
|
105568
|
+
// src/core/aggregation/recovery-spend.ts
|
|
105569
|
+
init_shim();
|
|
105570
|
+
init_secp256k1();
|
|
105571
|
+
var DUST_LIMIT_SATS = 546n;
|
|
105572
|
+
function bytesEqual2(a2, b) {
|
|
105573
|
+
return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
|
|
105574
|
+
}
|
|
105575
|
+
function buildRecoverySpend(params) {
|
|
105576
|
+
const {
|
|
105577
|
+
cohortKeys,
|
|
105578
|
+
recoverySecretKey,
|
|
105579
|
+
recoveryKey,
|
|
105580
|
+
recoverySequence,
|
|
105581
|
+
fallbackThreshold,
|
|
105582
|
+
fundingModel,
|
|
105583
|
+
network,
|
|
105584
|
+
utxo,
|
|
105585
|
+
destinationAddress,
|
|
105586
|
+
fee,
|
|
105587
|
+
beaconAddress
|
|
105588
|
+
} = params;
|
|
105589
|
+
if (cohortKeys.length === 0) {
|
|
105590
|
+
throw new AggregationCohortError(
|
|
105591
|
+
"Cannot build recovery spend: no cohort keys.",
|
|
105592
|
+
"NO_COHORT_KEYS"
|
|
105593
|
+
);
|
|
105594
|
+
}
|
|
105595
|
+
const derivedPub = schnorr2.getPublicKey(recoverySecretKey);
|
|
105596
|
+
if (!bytesEqual2(derivedPub, recoveryKey)) {
|
|
105597
|
+
throw new AggregationCohortError(
|
|
105598
|
+
"Recovery secret key does not correspond to the committed recovery key.",
|
|
105599
|
+
"RECOVERY_KEY_MISMATCH"
|
|
105600
|
+
);
|
|
105601
|
+
}
|
|
105602
|
+
const out = utxo.value - fee;
|
|
105603
|
+
if (out <= 0n) {
|
|
105604
|
+
throw new AggregationCohortError(
|
|
105605
|
+
`Recovery fee ${fee} exceeds UTXO value ${utxo.value}.`,
|
|
105606
|
+
"FEE_EXCEEDS_VALUE",
|
|
105607
|
+
{ fee: fee.toString(), value: utxo.value.toString() }
|
|
105608
|
+
);
|
|
105609
|
+
}
|
|
105610
|
+
if (out < DUST_LIMIT_SATS) {
|
|
105611
|
+
throw new AggregationCohortError(
|
|
105612
|
+
`Recovered output ${out} is below the dust limit ${DUST_LIMIT_SATS}; the spend would not relay.`,
|
|
105613
|
+
"DUST_OUTPUT",
|
|
105614
|
+
{ output: out.toString(), dustLimit: DUST_LIMIT_SATS.toString() }
|
|
105615
|
+
);
|
|
105616
|
+
}
|
|
105617
|
+
const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
|
|
105618
|
+
const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
|
|
105619
|
+
recoveryKey,
|
|
105620
|
+
recoverySequence,
|
|
105621
|
+
cohortKeys,
|
|
105622
|
+
fallbackThreshold: resolveFallbackThreshold(fallbackThreshold, cohortKeys.length)
|
|
105623
|
+
});
|
|
105624
|
+
const net = getNetwork(network);
|
|
105625
|
+
const payment = p2tr(internalKey, leaves, net, true);
|
|
105626
|
+
if (beaconAddress !== void 0 && payment.address !== beaconAddress) {
|
|
105627
|
+
throw new AggregationCohortError(
|
|
105628
|
+
`Reconstructed beacon address ${payment.address} does not match the cohort's funded address ${beaconAddress}.`,
|
|
105629
|
+
"BEACON_ADDRESS_MISMATCH",
|
|
105630
|
+
{ reconstructed: payment.address, expected: beaconAddress }
|
|
105631
|
+
);
|
|
105632
|
+
}
|
|
105633
|
+
const tx = new Transaction({ version: 2, allowUnknownInputs: true });
|
|
105634
|
+
tx.addInput({
|
|
105635
|
+
txid: utxo.txid,
|
|
105636
|
+
index: utxo.vout,
|
|
105637
|
+
witnessUtxo: { script: payment.script, amount: utxo.value },
|
|
105638
|
+
tapLeafScript: payment.tapLeafScript,
|
|
105639
|
+
sequence: recoverySequence
|
|
105640
|
+
});
|
|
105641
|
+
tx.addOutputAddress(destinationAddress, out, net);
|
|
105642
|
+
tx.signIdx(recoverySecretKey, 0);
|
|
105643
|
+
tx.finalize();
|
|
105644
|
+
return tx;
|
|
105645
|
+
}
|
|
105646
|
+
|
|
104825
105647
|
// src/core/aggregation/logger.ts
|
|
104826
105648
|
init_shim();
|
|
104827
105649
|
var CONSOLE_LOGGER = {
|
|
@@ -104854,6 +105676,7 @@ var KEYGEN_VALUES = /* @__PURE__ */ new Set([
|
|
|
104854
105676
|
]);
|
|
104855
105677
|
var UPDATE_VALUES = /* @__PURE__ */ new Set([
|
|
104856
105678
|
SUBMIT_UPDATE,
|
|
105679
|
+
SUBMIT_NONINCLUDED,
|
|
104857
105680
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
104858
105681
|
VALIDATION_ACK
|
|
104859
105682
|
]);
|
|
@@ -104861,7 +105684,9 @@ var SIGN_VALUES = /* @__PURE__ */ new Set([
|
|
|
104861
105684
|
AUTHORIZATION_REQUEST,
|
|
104862
105685
|
NONCE_CONTRIBUTION,
|
|
104863
105686
|
AGGREGATED_NONCE,
|
|
104864
|
-
SIGNATURE_AUTHORIZATION
|
|
105687
|
+
SIGNATURE_AUTHORIZATION,
|
|
105688
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
105689
|
+
FALLBACK_SIGNATURE
|
|
104865
105690
|
]);
|
|
104866
105691
|
function isAggregationMessageType(type) {
|
|
104867
105692
|
return KEYGEN_VALUES.has(type) || UPDATE_VALUES.has(type) || SIGN_VALUES.has(type);
|
|
@@ -111022,7 +111847,7 @@ var NostrTransport = class _NostrTransport {
|
|
|
111022
111847
|
* backfill historical events to late subscribers: republishing gives late
|
|
111023
111848
|
* joiners a window to discover the message without requiring protocol
|
|
111024
111849
|
* changes. Relay rate-limit / publish failures inside the interval are
|
|
111025
|
-
* caught and logged rather than propagated
|
|
111850
|
+
* caught and logged rather than propagated - the caller should stop the
|
|
111026
111851
|
* repeater once the protocol condition is satisfied.
|
|
111027
111852
|
*/
|
|
111028
111853
|
publishRepeating(message2, sender, intervalMs, recipient) {
|
|
@@ -111483,6 +112308,13 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111483
112308
|
#cohortTtlMs;
|
|
111484
112309
|
#phaseTimeoutMs;
|
|
111485
112310
|
#advertRepeatIntervalMs;
|
|
112311
|
+
#autoFallbackOnStall;
|
|
112312
|
+
/** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
|
|
112313
|
+
static #SIGNING_PHASES = [
|
|
112314
|
+
"SigningStarted" /* SigningStarted */,
|
|
112315
|
+
"NoncesCollected" /* NoncesCollected */,
|
|
112316
|
+
"AwaitingPartialSigs" /* AwaitingPartialSigs */
|
|
112317
|
+
];
|
|
111486
112318
|
/** Per-cohort run state, keyed by cohortId. */
|
|
111487
112319
|
#contexts = /* @__PURE__ */ new Map();
|
|
111488
112320
|
#handlersRegistered = false;
|
|
@@ -111500,6 +112332,7 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111500
112332
|
this.#cohortTtlMs = options2.cohortTtlMs;
|
|
111501
112333
|
this.#phaseTimeoutMs = options2.phaseTimeoutMs;
|
|
111502
112334
|
this.#advertRepeatIntervalMs = options2.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
112335
|
+
this.#autoFallbackOnStall = options2.autoFallbackOnStall ?? false;
|
|
111503
112336
|
this.session = new AggregationService({
|
|
111504
112337
|
// The coordinator never signs, so the state machine receives only the
|
|
111505
112338
|
// public half of the operator's keypair (see ADR 038). The full keypair
|
|
@@ -111661,11 +112494,42 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111661
112494
|
ctx.phaseTimer = void 0;
|
|
111662
112495
|
if (this.#phaseTimeoutMs === void 0) return;
|
|
111663
112496
|
ctx.phaseTimer = setTimeout(() => {
|
|
112497
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
112498
|
+
const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
|
|
112499
|
+
if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
|
|
112500
|
+
this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
|
|
112501
|
+
return;
|
|
112502
|
+
}
|
|
111664
112503
|
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
111665
112504
|
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
111666
112505
|
this.#failCohort(ctx, new Error(reason));
|
|
111667
112506
|
}, this.#phaseTimeoutMs);
|
|
111668
112507
|
}
|
|
112508
|
+
/**
|
|
112509
|
+
* Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
|
|
112510
|
+
* fallback (script-path) signatures instead (ADR 042). Idempotent and safe
|
|
112511
|
+
* against the optimistic completion: it commits the cohort to the fallback
|
|
112512
|
+
* path synchronously (the `committedPath` latch) before sending anything, so a
|
|
112513
|
+
* late optimistic signature can no longer complete-and-broadcast a competing
|
|
112514
|
+
* spend of the same UTXO. No-op if the cohort is unknown, already settled, or
|
|
112515
|
+
* already committed to a path.
|
|
112516
|
+
*
|
|
112517
|
+
* Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
|
|
112518
|
+
* set; otherwise call it from an operator decision (a UI "fall back now"
|
|
112519
|
+
* action). Throws only if the underlying state machine rejects the transition
|
|
112520
|
+
* (e.g. signing has not started).
|
|
112521
|
+
*/
|
|
112522
|
+
async triggerFallback(cohortId) {
|
|
112523
|
+
const ctx = this.#contexts.get(cohortId);
|
|
112524
|
+
if (!ctx || ctx.settled || ctx.committedPath) return;
|
|
112525
|
+
const messages2 = this.session.startFallbackSigning(cohortId);
|
|
112526
|
+
ctx.committedPath = "fallback";
|
|
112527
|
+
this.#stopAdvertRepeating(ctx);
|
|
112528
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
112529
|
+
const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
|
|
112530
|
+
this.emit("fallback-started", { cohortId, sessionId });
|
|
112531
|
+
await this.#sendAll(messages2);
|
|
112532
|
+
}
|
|
111669
112533
|
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
111670
112534
|
#onPhaseMaybeChanged(ctx) {
|
|
111671
112535
|
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
@@ -111754,13 +112618,15 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111754
112618
|
static #HANDLED_MESSAGE_TYPES = [
|
|
111755
112619
|
COHORT_OPT_IN,
|
|
111756
112620
|
SUBMIT_UPDATE,
|
|
112621
|
+
SUBMIT_NONINCLUDED,
|
|
111757
112622
|
VALIDATION_ACK,
|
|
111758
112623
|
NONCE_CONTRIBUTION,
|
|
111759
|
-
SIGNATURE_AUTHORIZATION
|
|
112624
|
+
SIGNATURE_AUTHORIZATION,
|
|
112625
|
+
FALLBACK_SIGNATURE
|
|
111760
112626
|
];
|
|
111761
112627
|
/**
|
|
111762
112628
|
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
111763
|
-
* are DID-scoped and cohort-agnostic
|
|
112629
|
+
* are DID-scoped and cohort-agnostic - one registration serves every cohort
|
|
111764
112630
|
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
111765
112631
|
* handler via the inbound message's cohortId.
|
|
111766
112632
|
*/
|
|
@@ -111769,9 +112635,11 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111769
112635
|
this.#handlersRegistered = true;
|
|
111770
112636
|
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
|
|
111771
112637
|
this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
|
|
112638
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
|
|
111772
112639
|
this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
|
|
111773
112640
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
111774
112641
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
112642
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
|
|
111775
112643
|
}
|
|
111776
112644
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
111777
112645
|
#unregisterHandlers() {
|
|
@@ -111932,6 +112800,32 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
111932
112800
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
111933
112801
|
*/
|
|
111934
112802
|
async #handleSignatureAuthorization(msg) {
|
|
112803
|
+
if (this.#stopped) return;
|
|
112804
|
+
const ctx = this.#contextFor(msg);
|
|
112805
|
+
if (!ctx) return;
|
|
112806
|
+
try {
|
|
112807
|
+
this.session.receive(msg);
|
|
112808
|
+
this.#drainRejections(ctx);
|
|
112809
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
112810
|
+
if (ctx.committedPath === "fallback") return;
|
|
112811
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
112812
|
+
if (result) {
|
|
112813
|
+
ctx.committedPath = "optimistic";
|
|
112814
|
+
this.#completeCohort(ctx, result);
|
|
112815
|
+
}
|
|
112816
|
+
} catch (err) {
|
|
112817
|
+
this.#failCohort(ctx, err);
|
|
112818
|
+
}
|
|
112819
|
+
}
|
|
112820
|
+
/**
|
|
112821
|
+
* Handler for receiving fallback (k-of-n script-path) signatures. The state
|
|
112822
|
+
* machine assembles and finalizes the fallback spend once k valid signatures
|
|
112823
|
+
* are in; the result is then emitted and the cohort's completion resolves. The
|
|
112824
|
+
* cohort is already committed to the fallback path (via {@link triggerFallback}).
|
|
112825
|
+
* @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
|
|
112826
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
112827
|
+
*/
|
|
112828
|
+
async #handleFallbackSignature(msg) {
|
|
111935
112829
|
if (this.#stopped) return;
|
|
111936
112830
|
const ctx = this.#contextFor(msg);
|
|
111937
112831
|
if (!ctx) return;
|
|
@@ -112005,7 +112899,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112005
112899
|
COHORT_READY,
|
|
112006
112900
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
112007
112901
|
AUTHORIZATION_REQUEST,
|
|
112008
|
-
AGGREGATED_NONCE
|
|
112902
|
+
AGGREGATED_NONCE,
|
|
112903
|
+
FALLBACK_AUTHORIZATION_REQUEST
|
|
112009
112904
|
];
|
|
112010
112905
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
112011
112906
|
#unregisterHandlers() {
|
|
@@ -112040,7 +112935,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112040
112935
|
*
|
|
112041
112936
|
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
112042
112937
|
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
112043
|
-
* `start()`, and listen for `cohort-complete`
|
|
112938
|
+
* `start()`, and listen for `cohort-complete` - the runner already drives
|
|
112044
112939
|
* any number of cohorts concurrently.
|
|
112045
112940
|
*
|
|
112046
112941
|
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
@@ -112075,6 +112970,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112075
112970
|
this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
|
|
112076
112971
|
this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
|
|
112077
112972
|
this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
|
|
112973
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
|
|
112078
112974
|
}
|
|
112079
112975
|
/**
|
|
112080
112976
|
* Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
|
|
@@ -112130,8 +113026,13 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112130
113026
|
cohortId,
|
|
112131
113027
|
beaconAddress: info.beaconAddress
|
|
112132
113028
|
});
|
|
112133
|
-
|
|
112134
|
-
|
|
113029
|
+
if (signedUpdate === null) {
|
|
113030
|
+
await this.#sendAll(this.session.declineUpdate(cohortId));
|
|
113031
|
+
this.emit("update-declined", { cohortId });
|
|
113032
|
+
} else {
|
|
113033
|
+
await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
|
|
113034
|
+
this.emit("update-submitted", { cohortId });
|
|
113035
|
+
}
|
|
112135
113036
|
} catch (err) {
|
|
112136
113037
|
this.emit("error", err);
|
|
112137
113038
|
}
|
|
@@ -112207,23 +113108,62 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112207
113108
|
const cohortId = msg.body?.cohortId;
|
|
112208
113109
|
if (!cohortId) return;
|
|
112209
113110
|
await this.#sendAll(this.session.generatePartialSignature(cohortId));
|
|
112210
|
-
|
|
112211
|
-
|
|
112212
|
-
|
|
112213
|
-
|
|
112214
|
-
|
|
112215
|
-
|
|
112216
|
-
|
|
112217
|
-
|
|
112218
|
-
|
|
112219
|
-
|
|
112220
|
-
|
|
112221
|
-
|
|
113111
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
113112
|
+
} catch (err) {
|
|
113113
|
+
this.emit("error", err);
|
|
113114
|
+
}
|
|
113115
|
+
}
|
|
113116
|
+
/**
|
|
113117
|
+
* Internal: handler for fallback authorization requests. The service abandoned
|
|
113118
|
+
* the optimistic key path; the member authorizes the k-of-n script-path spend
|
|
113119
|
+
* of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
|
|
113120
|
+
* the decision, signs the fallback, and completes once it has contributed (the
|
|
113121
|
+
* service needs only k members).
|
|
113122
|
+
* @param {BaseMessage} msg - The received fallback authorization request message.
|
|
113123
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
113124
|
+
*/
|
|
113125
|
+
async #handleFallbackAuthorizationRequest(msg) {
|
|
113126
|
+
if (this.#stopped) return;
|
|
113127
|
+
try {
|
|
113128
|
+
this.session.receive(msg);
|
|
113129
|
+
const cohortId = msg.body?.cohortId;
|
|
113130
|
+
if (!cohortId) return;
|
|
113131
|
+
const req = this.session.pendingFallbackRequests.get(cohortId);
|
|
113132
|
+
if (!req) return;
|
|
113133
|
+
this.emit("fallback-requested", req);
|
|
113134
|
+
const decision = await this.#onApproveSigning(req);
|
|
113135
|
+
if (!decision.approved) {
|
|
113136
|
+
this.emit("cohort-failed", { cohortId, reason: "Fallback signing rejected by participant" });
|
|
113137
|
+
return;
|
|
112222
113138
|
}
|
|
113139
|
+
await this.#sendAll(this.session.approveFallback(cohortId));
|
|
113140
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
112223
113141
|
} catch (err) {
|
|
112224
113142
|
this.emit("error", err);
|
|
112225
113143
|
}
|
|
112226
113144
|
}
|
|
113145
|
+
/**
|
|
113146
|
+
* Internal: emit `cohort-complete` with the participant's retained sidecar once
|
|
113147
|
+
* the cohort has reached the Complete phase. Surfaces the CAS Announcement map
|
|
113148
|
+
* (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
|
|
113149
|
+
* DID resolution. Read via getValidation (not pendingValidations, which lists
|
|
113150
|
+
* only the AwaitingValidation phase) so the sidecar is still available at
|
|
113151
|
+
* Complete. Shared by the optimistic and fallback completion paths.
|
|
113152
|
+
*/
|
|
113153
|
+
#emitCohortCompleteIfDone(cohortId) {
|
|
113154
|
+
if (this.session.getCohortPhase(cohortId) !== "Complete" /* Complete */) return;
|
|
113155
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
113156
|
+
if (!info) return;
|
|
113157
|
+
const validation = this.session.getValidation(cohortId);
|
|
113158
|
+
this.emit("cohort-complete", {
|
|
113159
|
+
cohortId,
|
|
113160
|
+
beaconAddress: info.beaconAddress,
|
|
113161
|
+
beaconType: validation?.beaconType ?? "",
|
|
113162
|
+
included: validation?.included ?? true,
|
|
113163
|
+
casAnnouncement: validation?.casAnnouncement,
|
|
113164
|
+
smtProof: validation?.smtProof
|
|
113165
|
+
});
|
|
113166
|
+
}
|
|
112227
113167
|
/**
|
|
112228
113168
|
* Internal: send helper to ensure messages are sent sequentially. This is important for protocol
|
|
112229
113169
|
* correctness, as some transports may not guarantee message order if sent in parallel.
|
|
@@ -112240,6 +113180,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
112240
113180
|
|
|
112241
113181
|
// src/core/aggregation/runner/aggregation-runner.ts
|
|
112242
113182
|
init_shim();
|
|
113183
|
+
init_utils();
|
|
112243
113184
|
var AggregationRunner = class {
|
|
112244
113185
|
/**
|
|
112245
113186
|
* Run a cohort of ONE participant entirely in-process and return the
|
|
@@ -112247,8 +113188,8 @@ var AggregationRunner = class {
|
|
|
112247
113188
|
*
|
|
112248
113189
|
* One party plays both the coordinating service and the lone participant,
|
|
112249
113190
|
* connected over an {@link InMemoryTransport} (no relay or HTTP server). This
|
|
112250
|
-
* makes the single-participant aggregate-beacon path
|
|
112251
|
-
* two-axis beacon matrix
|
|
113191
|
+
* makes the single-participant aggregate-beacon path (the N=1 corner of the
|
|
113192
|
+
* two-axis beacon matrix, see ADR 037) first-class, useful for generating
|
|
112252
113193
|
* and reproducing single-participant aggregate test vectors.
|
|
112253
113194
|
*
|
|
112254
113195
|
* The service advertises a cohort with `minParticipants: 1`; the participant
|
|
@@ -112266,11 +113207,13 @@ var AggregationRunner = class {
|
|
|
112266
113207
|
transport.registerPeer(options2.participant.did, options2.participant.keys.publicKey.compressed);
|
|
112267
113208
|
transport.registerPeer(options2.service.did, options2.service.keys.publicKey.compressed);
|
|
112268
113209
|
transport.start();
|
|
113210
|
+
const recoveryKey = options2.config.recoveryKey ?? bytesToHex(options2.service.keys.publicKey.compressed.slice(1));
|
|
113211
|
+
const recoverySequence = options2.config.recoverySequence ?? DEFAULT_RECOVERY_SEQUENCE;
|
|
112269
113212
|
const service = new AggregationServiceRunner({
|
|
112270
113213
|
transport,
|
|
112271
113214
|
did: options2.service.did,
|
|
112272
113215
|
keys: options2.service.keys,
|
|
112273
|
-
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType },
|
|
113216
|
+
config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType, recoveryKey, recoverySequence, fundingModel: DEFAULT_FUNDING_MODEL },
|
|
112274
113217
|
onProvideTxData: options2.onProvideTxData,
|
|
112275
113218
|
cohortTtlMs: options2.cohortTtlMs,
|
|
112276
113219
|
phaseTimeoutMs: options2.phaseTimeoutMs,
|
|
@@ -112400,8 +113343,7 @@ async function fetchSpendableUtxo(bitcoinAddress, bitcoin) {
|
|
|
112400
113343
|
async function buildAggregationBeaconTx(opts) {
|
|
112401
113344
|
const feeEstimator = opts.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
112402
113345
|
const { utxo, prevTxBytes } = await fetchSpendableUtxo(opts.beaconAddress, opts.bitcoin);
|
|
112403
|
-
const
|
|
112404
|
-
const witnessScript = tapOut.script;
|
|
113346
|
+
const witnessScript = OutScript.encode(Address(opts.network).decode(opts.beaconAddress));
|
|
112405
113347
|
const feeSats = await feeEstimator.estimateFee(P2TR_BEACON_TX_VSIZE);
|
|
112406
113348
|
if (BigInt(utxo.value) <= feeSats) {
|
|
112407
113349
|
throw new BeaconError(
|
|
@@ -112480,7 +113422,7 @@ var SinglePartyBeacon = class {
|
|
|
112480
113422
|
*
|
|
112481
113423
|
* Composed from the three extracted phases ({@link buildSinglePartyTx},
|
|
112482
113424
|
* {@link signSinglePartyTx}, {@link broadcastRawTx}) so each piece can be exercised
|
|
112483
|
-
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead
|
|
113425
|
+
* in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead:
|
|
112484
113426
|
* the multi-party path can't share the signing phase, but the tx-construction
|
|
112485
113427
|
* plumbing (UTXO fetch + OP_RETURN output + change output) is shared.
|
|
112486
113428
|
*
|
|
@@ -112512,7 +113454,7 @@ var SinglePartyBeacon = class {
|
|
|
112512
113454
|
*
|
|
112513
113455
|
* Detects the beacon address script kind (P2PKH / P2WPKH / P2TR) and configures
|
|
112514
113456
|
* the input accordingly. Validates that the signer's pubkey produces the beacon
|
|
112515
|
-
* address under that script kind
|
|
113457
|
+
* address under that script kind: without this check, a misconfigured caller
|
|
112516
113458
|
* would burn a real UTXO on a tx that fails at broadcast. Fees are computed from
|
|
112517
113459
|
* the per-kind {@link SINGLETON_BEACON_TX_VSIZE} constant, avoiding any probe-sign
|
|
112518
113460
|
* round-trip.
|
|
@@ -128399,7 +129341,7 @@ var Updater = class _Updater {
|
|
|
128399
129341
|
#verificationMethod;
|
|
128400
129342
|
#beaconService;
|
|
128401
129343
|
/**
|
|
128402
|
-
* @internal
|
|
129344
|
+
* @internal Use {@link DidBtcr2.update} to create instances.
|
|
128403
129345
|
*/
|
|
128404
129346
|
constructor(params) {
|
|
128405
129347
|
this.#sourceDocument = params.sourceDocument;
|
|
@@ -128436,7 +129378,7 @@ var Updater = class _Updater {
|
|
|
128436
129378
|
const targetDocument = JSONPatch.apply(sourceDocument, patches);
|
|
128437
129379
|
if (targetDocument.id !== sourceDocument.id) {
|
|
128438
129380
|
throw new UpdateError(
|
|
128439
|
-
`Patches must not change the DID document id (source "${sourceDocument.id}"
|
|
129381
|
+
`Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
|
|
128440
129382
|
INVALID_DID_UPDATE,
|
|
128441
129383
|
{ sourceId: sourceDocument.id, targetId: targetDocument.id }
|
|
128442
129384
|
);
|
|
@@ -128520,8 +129462,8 @@ var Updater = class _Updater {
|
|
|
128520
129462
|
}
|
|
128521
129463
|
/**
|
|
128522
129464
|
* Advance the state machine. Returns either:
|
|
128523
|
-
* - `{ status: 'action-required', needs }`
|
|
128524
|
-
* - `{ status: 'complete', result }`
|
|
129465
|
+
* - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
|
|
129466
|
+
* - `{ status: 'complete', result }` update is signed and broadcast
|
|
128525
129467
|
*/
|
|
128526
129468
|
advance() {
|
|
128527
129469
|
while (true) {
|
|
@@ -128534,7 +129476,7 @@ var Updater = class _Updater {
|
|
|
128534
129476
|
continue;
|
|
128535
129477
|
}
|
|
128536
129478
|
// Phase: Sign
|
|
128537
|
-
// Emit NeedSigningKey
|
|
129479
|
+
// Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
|
|
128538
129480
|
case "Sign": {
|
|
128539
129481
|
return {
|
|
128540
129482
|
status: "action-required",
|
|
@@ -128717,8 +129659,8 @@ var DidBtcr2 = class {
|
|
|
128717
129659
|
*
|
|
128718
129660
|
* Factory method that validates the update parameters and returns a sans-I/O
|
|
128719
129661
|
* {@link Updater} state machine. The caller drives the updater through its
|
|
128720
|
-
* phases (Construct
|
|
128721
|
-
* `provide()`. The method package performs **zero I/O
|
|
129662
|
+
* phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
|
|
129663
|
+
* `provide()`. The method package performs **zero I/O**: signing key retrieval
|
|
128722
129664
|
* (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
|
|
128723
129665
|
*
|
|
128724
129666
|
* For a fully-wired version with Bitcoin broadcast and key handling, see
|
|
@@ -128836,7 +129778,7 @@ var Resolver = class _Resolver {
|
|
|
128836
129778
|
#unsortedUpdates = [];
|
|
128837
129779
|
#resolvedResponse = null;
|
|
128838
129780
|
/**
|
|
128839
|
-
* @internal
|
|
129781
|
+
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
128840
129782
|
*/
|
|
128841
129783
|
constructor(didComponents, sidecarData, currentDocument, options2) {
|
|
128842
129784
|
this.#didComponents = didComponents;
|
|
@@ -129072,8 +130014,8 @@ var Resolver = class _Resolver {
|
|
|
129072
130014
|
}
|
|
129073
130015
|
/**
|
|
129074
130016
|
* Advance the state machine. Returns either:
|
|
129075
|
-
* - `{ status: 'action-required', needs }`
|
|
129076
|
-
* - `{ status: 'resolved', result }`
|
|
130017
|
+
* - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
|
|
130018
|
+
* - `{ status: 'resolved', result }` - resolution complete
|
|
129077
130019
|
*
|
|
129078
130020
|
* Analogous to Rust's `Resolver::resolve()`.
|
|
129079
130021
|
*/
|
|
@@ -129313,9 +130255,11 @@ export {
|
|
|
129313
130255
|
DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
|
|
129314
130256
|
DEFAULT_BROADCAST_LOOKBACK_MS,
|
|
129315
130257
|
DEFAULT_CLOCK_SKEW_SEC,
|
|
130258
|
+
DEFAULT_FUNDING_MODEL,
|
|
129316
130259
|
DEFAULT_MAX_UPDATE_SIZE_BYTES,
|
|
129317
130260
|
DEFAULT_NONCE_LEN_BYTES,
|
|
129318
130261
|
DEFAULT_NOSTR_RELAYS,
|
|
130262
|
+
DEFAULT_RECOVERY_SEQUENCE,
|
|
129319
130263
|
DID_REGEX,
|
|
129320
130264
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
129321
130265
|
DidBtcr2,
|
|
@@ -129324,6 +130268,8 @@ export {
|
|
|
129324
130268
|
DidDocumentBuilder,
|
|
129325
130269
|
DidVerificationMethod,
|
|
129326
130270
|
Document,
|
|
130271
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
130272
|
+
FALLBACK_SIGNATURE,
|
|
129327
130273
|
GenesisDocument,
|
|
129328
130274
|
HTTP_ENVELOPE_VERSION,
|
|
129329
130275
|
HTTP_ROUTE,
|
|
@@ -129337,7 +130283,9 @@ export {
|
|
|
129337
130283
|
InMemoryTransport,
|
|
129338
130284
|
InboxBuffer,
|
|
129339
130285
|
KNOWN_BEACON_TYPES,
|
|
130286
|
+
KNOWN_FUNDING_MODELS,
|
|
129340
130287
|
KeyPairAggregationSigner,
|
|
130288
|
+
MAX_RECOVERY_SEQUENCE,
|
|
129341
130289
|
NONCE_CONTRIBUTION,
|
|
129342
130290
|
NonceCache,
|
|
129343
130291
|
NostrTransport,
|
|
@@ -129354,6 +130302,7 @@ export {
|
|
|
129354
130302
|
SMTBeacon,
|
|
129355
130303
|
SMTBeaconError,
|
|
129356
130304
|
SSE_EVENT,
|
|
130305
|
+
SUBMIT_NONINCLUDED,
|
|
129357
130306
|
SUBMIT_UPDATE,
|
|
129358
130307
|
ServiceCohortPhase,
|
|
129359
130308
|
SigningSessionError,
|
|
@@ -129362,6 +130311,7 @@ export {
|
|
|
129362
130311
|
SingletonBeacon,
|
|
129363
130312
|
SingletonBeaconError,
|
|
129364
130313
|
StaticFeeEstimator,
|
|
130314
|
+
TAPROOT_LEAF_VERSION,
|
|
129365
130315
|
TransportAdapterError,
|
|
129366
130316
|
TransportError,
|
|
129367
130317
|
TransportFactory,
|
|
@@ -129369,6 +130319,11 @@ export {
|
|
|
129369
130319
|
Updater,
|
|
129370
130320
|
VALIDATION_ACK,
|
|
129371
130321
|
buildAggregationBeaconTx,
|
|
130322
|
+
buildFallbackLeaf,
|
|
130323
|
+
buildFallbackSpend,
|
|
130324
|
+
buildRecoveryLeaves,
|
|
130325
|
+
buildRecoveryScript,
|
|
130326
|
+
buildRecoverySpend,
|
|
129372
130327
|
buildRequestAuth,
|
|
129373
130328
|
createAggregatedNonceMessage,
|
|
129374
130329
|
createAuthorizationRequestMessage,
|
|
@@ -129377,13 +130332,17 @@ export {
|
|
|
129377
130332
|
createCohortOptInMessage,
|
|
129378
130333
|
createCohortReadyMessage,
|
|
129379
130334
|
createDistributeAggregatedDataMessage,
|
|
130335
|
+
createFallbackAuthorizationRequestMessage,
|
|
130336
|
+
createFallbackSignatureMessage,
|
|
129380
130337
|
createNonceContributionMessage,
|
|
129381
130338
|
createSignatureAuthorizationMessage,
|
|
130339
|
+
createSubmitNonIncludedMessage,
|
|
129382
130340
|
createSubmitUpdateMessage,
|
|
129383
130341
|
createValidationAckMessage,
|
|
129384
130342
|
defaultReconnectBackoff,
|
|
129385
130343
|
deriveSingletonAddress,
|
|
129386
130344
|
detectSingletonScriptKind,
|
|
130345
|
+
fallbackSighash,
|
|
129387
130346
|
formatSseComment,
|
|
129388
130347
|
formatSseEvent,
|
|
129389
130348
|
getBeaconStrategy,
|
|
@@ -129395,10 +130354,13 @@ export {
|
|
|
129395
130354
|
isCohortOptInMessage,
|
|
129396
130355
|
isCohortReadyMessage,
|
|
129397
130356
|
isDistributeAggregatedDataMessage,
|
|
130357
|
+
isFallbackAuthorizationRequestMessage,
|
|
130358
|
+
isFallbackSignatureMessage,
|
|
129398
130359
|
isKeygenMessageType,
|
|
129399
130360
|
isNonceContributionMessage,
|
|
129400
130361
|
isSignMessageType,
|
|
129401
130362
|
isSignatureAuthorizationMessage,
|
|
130363
|
+
isSubmitNonIncludedMessage,
|
|
129402
130364
|
isSubmitUpdateMessage,
|
|
129403
130365
|
isUpdateMessageType,
|
|
129404
130366
|
isValidationAckMessage,
|
|
@@ -129407,8 +130369,10 @@ export {
|
|
|
129407
130369
|
parseRequestAuth,
|
|
129408
130370
|
parseSseStream,
|
|
129409
130371
|
registerBeaconStrategy,
|
|
130372
|
+
resolveFallbackThreshold,
|
|
129410
130373
|
reviveFromWire,
|
|
129411
130374
|
signEnvelope,
|
|
130375
|
+
tapLeafHash2 as tapLeafHash,
|
|
129412
130376
|
validateCohortConditions,
|
|
129413
130377
|
verifyEnvelope,
|
|
129414
130378
|
verifyRequestAuth
|