@did-btcr2/aggregation 0.1.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/LICENSE +373 -0
- package/README.md +19 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/browser.js +24017 -0
- package/dist/browser.mjs +24011 -0
- package/dist/cjs/index.js +5690 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/beacon-strategy.js +76 -0
- package/dist/esm/beacon-strategy.js.map +1 -0
- package/dist/esm/cohort.js +316 -0
- package/dist/esm/cohort.js.map +1 -0
- package/dist/esm/conditions.js +102 -0
- package/dist/esm/conditions.js.map +1 -0
- package/dist/esm/errors.js +22 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/fallback-spend.js +132 -0
- package/dist/esm/fallback-spend.js.map +1 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger.js +15 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/messages/base.js +36 -0
- package/dist/esm/messages/base.js.map +1 -0
- package/dist/esm/messages/bodies.js +149 -0
- package/dist/esm/messages/bodies.js.map +1 -0
- package/dist/esm/messages/constants.js +37 -0
- package/dist/esm/messages/constants.js.map +1 -0
- package/dist/esm/messages/factories.js +139 -0
- package/dist/esm/messages/factories.js.map +1 -0
- package/dist/esm/messages/guards.js +40 -0
- package/dist/esm/messages/guards.js.map +1 -0
- package/dist/esm/messages/index.js +6 -0
- package/dist/esm/messages/index.js.map +1 -0
- package/dist/esm/participant.js +588 -0
- package/dist/esm/participant.js.map +1 -0
- package/dist/esm/phases.js +45 -0
- package/dist/esm/phases.js.map +1 -0
- package/dist/esm/recovery-policy.js +156 -0
- package/dist/esm/recovery-policy.js.map +1 -0
- package/dist/esm/recovery-spend.js +106 -0
- package/dist/esm/recovery-spend.js.map +1 -0
- package/dist/esm/runner/aggregation-runner.js +75 -0
- package/dist/esm/runner/aggregation-runner.js.map +1 -0
- package/dist/esm/runner/events.js +2 -0
- package/dist/esm/runner/events.js.map +1 -0
- package/dist/esm/runner/index.js +6 -0
- package/dist/esm/runner/index.js.map +1 -0
- package/dist/esm/runner/participant-runner.js +407 -0
- package/dist/esm/runner/participant-runner.js.map +1 -0
- package/dist/esm/runner/service-runner.js +701 -0
- package/dist/esm/runner/service-runner.js.map +1 -0
- package/dist/esm/runner/typed-emitter.js +80 -0
- package/dist/esm/runner/typed-emitter.js.map +1 -0
- package/dist/esm/service.js +774 -0
- package/dist/esm/service.js.map +1 -0
- package/dist/esm/signer.js +33 -0
- package/dist/esm/signer.js.map +1 -0
- package/dist/esm/signing-session.js +195 -0
- package/dist/esm/signing-session.js.map +1 -0
- package/dist/esm/transport/didcomm.js +41 -0
- package/dist/esm/transport/didcomm.js.map +1 -0
- package/dist/esm/transport/error.js +12 -0
- package/dist/esm/transport/error.js.map +1 -0
- package/dist/esm/transport/factory.js +29 -0
- package/dist/esm/transport/factory.js.map +1 -0
- package/dist/esm/transport/http/client.js +342 -0
- package/dist/esm/transport/http/client.js.map +1 -0
- package/dist/esm/transport/http/envelope.js +126 -0
- package/dist/esm/transport/http/envelope.js.map +1 -0
- package/dist/esm/transport/http/errors.js +11 -0
- package/dist/esm/transport/http/errors.js.map +1 -0
- package/dist/esm/transport/http/inbox-buffer.js +45 -0
- package/dist/esm/transport/http/inbox-buffer.js.map +1 -0
- package/dist/esm/transport/http/index.js +12 -0
- package/dist/esm/transport/http/index.js.map +1 -0
- package/dist/esm/transport/http/nonce-cache.js +38 -0
- package/dist/esm/transport/http/nonce-cache.js.map +1 -0
- package/dist/esm/transport/http/protocol.js +28 -0
- package/dist/esm/transport/http/protocol.js.map +1 -0
- package/dist/esm/transport/http/rate-limiter.js +45 -0
- package/dist/esm/transport/http/rate-limiter.js.map +1 -0
- package/dist/esm/transport/http/request-auth.js +100 -0
- package/dist/esm/transport/http/request-auth.js.map +1 -0
- package/dist/esm/transport/http/server.js +475 -0
- package/dist/esm/transport/http/server.js.map +1 -0
- package/dist/esm/transport/http/sse-stream.js +110 -0
- package/dist/esm/transport/http/sse-stream.js.map +1 -0
- package/dist/esm/transport/http/sse-writer.js +25 -0
- package/dist/esm/transport/http/sse-writer.js.map +1 -0
- package/dist/esm/transport/in-memory.js +149 -0
- package/dist/esm/transport/in-memory.js.map +1 -0
- package/dist/esm/transport/index.js +8 -0
- package/dist/esm/transport/index.js.map +1 -0
- package/dist/esm/transport/nostr.js +491 -0
- package/dist/esm/transport/nostr.js.map +1 -0
- package/dist/esm/transport/transport.js +2 -0
- package/dist/esm/transport/transport.js.map +1 -0
- package/dist/types/beacon-strategy.d.ts +56 -0
- package/dist/types/beacon-strategy.d.ts.map +1 -0
- package/dist/types/cohort.d.ts +188 -0
- package/dist/types/cohort.d.ts.map +1 -0
- package/dist/types/conditions.d.ts +86 -0
- package/dist/types/conditions.d.ts.map +1 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/fallback-spend.d.ts +94 -0
- package/dist/types/fallback-spend.d.ts.map +1 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger.d.ts +22 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/messages/base.d.ts +58 -0
- package/dist/types/messages/base.d.ts.map +1 -0
- package/dist/types/messages/bodies.d.ts +177 -0
- package/dist/types/messages/bodies.d.ts.map +1 -0
- package/dist/types/messages/constants.d.ts +31 -0
- package/dist/types/messages/constants.d.ts.map +1 -0
- package/dist/types/messages/factories.d.ts +217 -0
- package/dist/types/messages/factories.d.ts.map +1 -0
- package/dist/types/messages/guards.d.ts +11 -0
- package/dist/types/messages/guards.d.ts.map +1 -0
- package/dist/types/messages/index.d.ts +6 -0
- package/dist/types/messages/index.d.ts.map +1 -0
- package/dist/types/participant.d.ts +162 -0
- package/dist/types/participant.d.ts.map +1 -0
- package/dist/types/phases.d.ts +55 -0
- package/dist/types/phases.d.ts.map +1 -0
- package/dist/types/recovery-policy.d.ts +132 -0
- package/dist/types/recovery-policy.d.ts.map +1 -0
- package/dist/types/recovery-spend.d.ts +94 -0
- package/dist/types/recovery-spend.d.ts.map +1 -0
- package/dist/types/runner/aggregation-runner.d.ts +72 -0
- package/dist/types/runner/aggregation-runner.d.ts.map +1 -0
- package/dist/types/runner/events.d.ts +147 -0
- package/dist/types/runner/events.d.ts.map +1 -0
- package/dist/types/runner/index.d.ts +6 -0
- package/dist/types/runner/index.d.ts.map +1 -0
- package/dist/types/runner/participant-runner.d.ts +122 -0
- package/dist/types/runner/participant-runner.d.ts.map +1 -0
- package/dist/types/runner/service-runner.d.ts +228 -0
- package/dist/types/runner/service-runner.d.ts.map +1 -0
- package/dist/types/runner/typed-emitter.d.ts +41 -0
- package/dist/types/runner/typed-emitter.d.ts.map +1 -0
- package/dist/types/service.d.ts +177 -0
- package/dist/types/service.d.ts.map +1 -0
- package/dist/types/signer.d.ts +50 -0
- package/dist/types/signer.d.ts.map +1 -0
- package/dist/types/signing-session.d.ts +81 -0
- package/dist/types/signing-session.d.ts.map +1 -0
- package/dist/types/transport/didcomm.d.ts +23 -0
- package/dist/types/transport/didcomm.d.ts.map +1 -0
- package/dist/types/transport/error.d.ts +8 -0
- package/dist/types/transport/error.d.ts.map +1 -0
- package/dist/types/transport/factory.d.ts +28 -0
- package/dist/types/transport/factory.d.ts.map +1 -0
- package/dist/types/transport/http/client.d.ts +56 -0
- package/dist/types/transport/http/client.d.ts.map +1 -0
- package/dist/types/transport/http/envelope.d.ts +64 -0
- package/dist/types/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/transport/http/errors.d.ts +9 -0
- package/dist/types/transport/http/errors.d.ts.map +1 -0
- package/dist/types/transport/http/inbox-buffer.d.ts +32 -0
- package/dist/types/transport/http/inbox-buffer.d.ts.map +1 -0
- package/dist/types/transport/http/index.d.ts +12 -0
- package/dist/types/transport/http/index.d.ts.map +1 -0
- package/dist/types/transport/http/nonce-cache.d.ts +26 -0
- package/dist/types/transport/http/nonce-cache.d.ts.map +1 -0
- package/dist/types/transport/http/protocol.d.ts +53 -0
- package/dist/types/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/transport/http/rate-limiter.d.ts +41 -0
- package/dist/types/transport/http/rate-limiter.d.ts.map +1 -0
- package/dist/types/transport/http/request-auth.d.ts +50 -0
- package/dist/types/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/transport/http/server.d.ts +118 -0
- package/dist/types/transport/http/server.d.ts.map +1 -0
- package/dist/types/transport/http/sse-stream.d.ts +34 -0
- package/dist/types/transport/http/sse-stream.d.ts.map +1 -0
- package/dist/types/transport/http/sse-writer.d.ts +12 -0
- package/dist/types/transport/http/sse-writer.d.ts.map +1 -0
- package/dist/types/transport/in-memory.d.ts +64 -0
- package/dist/types/transport/in-memory.d.ts.map +1 -0
- package/dist/types/transport/index.d.ts +8 -0
- package/dist/types/transport/index.d.ts.map +1 -0
- package/dist/types/transport/nostr.d.ts +153 -0
- package/dist/types/transport/nostr.d.ts.map +1 -0
- package/dist/types/transport/transport.d.ts +62 -0
- package/dist/types/transport/transport.d.ts.map +1 -0
- package/package.json +128 -0
- package/src/beacon-strategy.ts +142 -0
- package/src/cohort.ts +422 -0
- package/src/conditions.ts +172 -0
- package/src/errors.ts +25 -0
- package/src/fallback-spend.ts +223 -0
- package/src/index.ts +23 -0
- package/src/logger.ts +33 -0
- package/src/messages/base.ts +80 -0
- package/src/messages/bodies.ts +328 -0
- package/src/messages/constants.ts +40 -0
- package/src/messages/factories.ts +297 -0
- package/src/messages/guards.ts +61 -0
- package/src/messages/index.ts +5 -0
- package/src/participant.ts +779 -0
- package/src/phases.ts +91 -0
- package/src/recovery-policy.ts +237 -0
- package/src/recovery-spend.ts +192 -0
- package/src/runner/aggregation-runner.ts +125 -0
- package/src/runner/events.ts +129 -0
- package/src/runner/index.ts +5 -0
- package/src/runner/participant-runner.ts +488 -0
- package/src/runner/service-runner.ts +891 -0
- package/src/runner/typed-emitter.ts +87 -0
- package/src/service.ts +1000 -0
- package/src/signer.ts +67 -0
- package/src/signing-session.ts +290 -0
- package/src/transport/didcomm.ts +59 -0
- package/src/transport/error.ts +13 -0
- package/src/transport/factory.ts +65 -0
- package/src/transport/http/client.ts +409 -0
- package/src/transport/http/envelope.ts +204 -0
- package/src/transport/http/errors.ts +11 -0
- package/src/transport/http/inbox-buffer.ts +53 -0
- package/src/transport/http/index.ts +11 -0
- package/src/transport/http/nonce-cache.ts +43 -0
- package/src/transport/http/protocol.ts +57 -0
- package/src/transport/http/rate-limiter.ts +75 -0
- package/src/transport/http/request-auth.ts +164 -0
- package/src/transport/http/server.ts +617 -0
- package/src/transport/http/sse-stream.ts +121 -0
- package/src/transport/http/sse-writer.ts +23 -0
- package/src/transport/in-memory.ts +178 -0
- package/src/transport/index.ts +7 -0
- package/src/transport/nostr.ts +576 -0
- package/src/transport/transport.ts +79 -0
package/src/signer.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
2
|
+
import { wipe } from '@did-btcr2/keypair';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A signing capability for MuSig2 aggregation.
|
|
6
|
+
*
|
|
7
|
+
* MuSig2 (BIP-327) cannot be driven through a generic `sign(message)` primitive:
|
|
8
|
+
* both nonce generation and partial signing need the raw 32-byte secret scalar
|
|
9
|
+
* (see ADR 038). Rather than hand that scalar around as a long-lived field, the
|
|
10
|
+
* participant holds an `AggregationSigner` and materializes the secret only for
|
|
11
|
+
* the duration of a single operation via {@link AggregationSigner.withSecret},
|
|
12
|
+
* which is responsible for wiping its working copy afterward.
|
|
13
|
+
*
|
|
14
|
+
* This is the seam a non-extractable / KMS-backed signer (or a session-scoped
|
|
15
|
+
* signer that destroys its key after the cohort completes) plugs into without
|
|
16
|
+
* changing the participant state machine.
|
|
17
|
+
*
|
|
18
|
+
* @interface AggregationSigner
|
|
19
|
+
*/
|
|
20
|
+
export interface AggregationSigner {
|
|
21
|
+
/** Compressed 33-byte public key. Not secret; always available. */
|
|
22
|
+
readonly publicKey: Uint8Array;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Materialize the raw 32-byte secret key, pass it to `fn`, and zeroize the
|
|
26
|
+
* working copy before returning - even if `fn` throws. The secret must not
|
|
27
|
+
* escape the callback.
|
|
28
|
+
*
|
|
29
|
+
* @typeParam T The callback's return type (e.g. a nonce contribution or partial signature).
|
|
30
|
+
* @param {(secretKey: Uint8Array) => T} fn Operation that needs the raw secret for its duration.
|
|
31
|
+
* @returns {T} Whatever `fn` returns.
|
|
32
|
+
*/
|
|
33
|
+
withSecret<T>(fn: (secretKey: Uint8Array) => T): T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* {@link AggregationSigner} backed by an in-memory {@link SchnorrKeyPair}.
|
|
38
|
+
*
|
|
39
|
+
* The keypair is held privately (never exposed as a public field) and each
|
|
40
|
+
* `withSecret` call pulls a fresh copy of the secret bytes, hands it to the
|
|
41
|
+
* callback, and wipes that copy on return. The underlying keypair is the
|
|
42
|
+
* caller's to own and destroy; this signer never mutates or destroys it.
|
|
43
|
+
*
|
|
44
|
+
* @class KeyPairAggregationSigner
|
|
45
|
+
* @implements {AggregationSigner}
|
|
46
|
+
*/
|
|
47
|
+
export class KeyPairAggregationSigner implements AggregationSigner {
|
|
48
|
+
readonly publicKey: Uint8Array;
|
|
49
|
+
readonly #keys: SchnorrKeyPair;
|
|
50
|
+
|
|
51
|
+
/** @param {SchnorrKeyPair} keys The keypair whose secret backs this signer. */
|
|
52
|
+
constructor(keys: SchnorrKeyPair) {
|
|
53
|
+
this.#keys = keys;
|
|
54
|
+
this.publicKey = keys.publicKey.compressed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
withSecret<T>(fn: (secretKey: Uint8Array) => T): T {
|
|
58
|
+
// `.bytes` returns a fresh copy; wipe it once the operation completes so the
|
|
59
|
+
// raw scalar does not linger on the heap beyond a single MuSig2 step.
|
|
60
|
+
const secretKey = this.#keys.secretKey.bytes;
|
|
61
|
+
try {
|
|
62
|
+
return fn(secretKey);
|
|
63
|
+
} finally {
|
|
64
|
+
wipe(secretKey);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { wipe } from '@did-btcr2/keypair';
|
|
2
|
+
import type { Transaction } from '@scure/btc-signer';
|
|
3
|
+
import { SigHash } from '@scure/btc-signer';
|
|
4
|
+
import * as musig2 from '@scure/btc-signer/musig2';
|
|
5
|
+
import type { AggregationCohort } from './cohort.js';
|
|
6
|
+
import { SigningSessionError } from './errors.js';
|
|
7
|
+
import type { SigningSessionPhaseType } from './phases.js';
|
|
8
|
+
import { SigningSessionPhase } from './phases.js';
|
|
9
|
+
|
|
10
|
+
type PublicKeyHex = string;
|
|
11
|
+
type Nonce = Uint8Array;
|
|
12
|
+
|
|
13
|
+
export interface SigningSessionParams {
|
|
14
|
+
id?: string;
|
|
15
|
+
cohort: AggregationCohort;
|
|
16
|
+
pendingTx: Transaction;
|
|
17
|
+
prevOutScripts?: Uint8Array[];
|
|
18
|
+
prevOutValues?: bigint[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MuSig2 signing session for a beacon transaction.
|
|
23
|
+
*
|
|
24
|
+
* Implements the BIP-327 signing protocol over a Taproot key-path-only
|
|
25
|
+
* transaction. The session is used by both AggregationService (collecting
|
|
26
|
+
* nonces and partial signatures, computing the final signature) and
|
|
27
|
+
* AggregationParticipant (generating their nonce and partial signature).
|
|
28
|
+
*
|
|
29
|
+
* @class BeaconSigningSession
|
|
30
|
+
*/
|
|
31
|
+
export class BeaconSigningSession {
|
|
32
|
+
/** Unique identifier for this signing session. */
|
|
33
|
+
public id: string;
|
|
34
|
+
|
|
35
|
+
/** The cohort this session signs for. */
|
|
36
|
+
public cohort: AggregationCohort;
|
|
37
|
+
|
|
38
|
+
/** The Bitcoin transaction being signed. */
|
|
39
|
+
public pendingTx: Transaction;
|
|
40
|
+
|
|
41
|
+
/** Previous output scripts for Taproot sighash computation (BIP-341). */
|
|
42
|
+
public prevOutScripts: Uint8Array[];
|
|
43
|
+
|
|
44
|
+
/** Previous output values for Taproot sighash computation. */
|
|
45
|
+
public prevOutValues: bigint[];
|
|
46
|
+
|
|
47
|
+
/** Map of participant publicKey-hex to public nonce contribution. */
|
|
48
|
+
public nonceContributions: Map<PublicKeyHex, Nonce> = new Map();
|
|
49
|
+
|
|
50
|
+
/** Aggregated MuSig2 nonce (66 bytes). */
|
|
51
|
+
public aggregatedNonce?: Uint8Array;
|
|
52
|
+
|
|
53
|
+
/** Map of participant DID to partial signature. */
|
|
54
|
+
public partialSignatures: Map<string, Uint8Array> = new Map();
|
|
55
|
+
|
|
56
|
+
/** Final 64-byte Schnorr signature. */
|
|
57
|
+
public signature?: Uint8Array;
|
|
58
|
+
|
|
59
|
+
/** Current signing session phase. */
|
|
60
|
+
public phase: SigningSessionPhaseType;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Participant's MuSig2 secret nonce, held only on the participant side
|
|
64
|
+
* between {@link generateNonceContribution} and {@link generatePartialSignature}.
|
|
65
|
+
* Private and cleared on every terminal path (success, failure, or teardown
|
|
66
|
+
* via {@link clearSecrets}) so a spent or abandoned nonce cannot be reused or
|
|
67
|
+
* serialized.
|
|
68
|
+
*/
|
|
69
|
+
#secretNonce?: Uint8Array;
|
|
70
|
+
|
|
71
|
+
constructor({ id, cohort, pendingTx, prevOutScripts, prevOutValues }: SigningSessionParams) {
|
|
72
|
+
this.id = id || crypto.randomUUID();
|
|
73
|
+
this.cohort = cohort;
|
|
74
|
+
this.pendingTx = pendingTx;
|
|
75
|
+
this.prevOutScripts = prevOutScripts || [];
|
|
76
|
+
this.prevOutValues = prevOutValues || [];
|
|
77
|
+
this.phase = SigningSessionPhase.AwaitingNonceContributions;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Computes the Taproot sighash (BIP-341) for the first input.
|
|
82
|
+
*/
|
|
83
|
+
get sigHash(): Uint8Array {
|
|
84
|
+
if(!this.prevOutScripts.length || !this.prevOutValues.length) {
|
|
85
|
+
throw new SigningSessionError(
|
|
86
|
+
'Cannot compute sighash: missing prevOutScripts or prevOutValues.',
|
|
87
|
+
'SIGHASH_ERROR'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return this.pendingTx.preimageWitnessV1(
|
|
91
|
+
0,
|
|
92
|
+
this.prevOutScripts,
|
|
93
|
+
SigHash.DEFAULT,
|
|
94
|
+
this.prevOutValues
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public addNonceContribution(participantDid: string, nonceContribution: Uint8Array): void {
|
|
99
|
+
if(this.phase !== SigningSessionPhase.AwaitingNonceContributions) {
|
|
100
|
+
throw new SigningSessionError(
|
|
101
|
+
`Nonce contributions not expected. Current phase: ${this.phase}`,
|
|
102
|
+
'INVALID_PHASE', { phase: this.phase }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if(!this.cohort.participants.includes(participantDid)) {
|
|
106
|
+
throw new SigningSessionError(
|
|
107
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
108
|
+
'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if(nonceContribution.length !== 66) {
|
|
112
|
+
throw new SigningSessionError(
|
|
113
|
+
`Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
|
|
114
|
+
'INVALID_NONCE_LENGTH'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if(this.nonceContributions.has(participantDid)) {
|
|
118
|
+
throw new SigningSessionError(
|
|
119
|
+
`Duplicate nonce contribution from ${participantDid}.`,
|
|
120
|
+
'DUPLICATE_NONCE'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
this.nonceContributions.set(participantDid, nonceContribution);
|
|
124
|
+
|
|
125
|
+
if(this.nonceContributions.size === this.cohort.participants.length) {
|
|
126
|
+
this.phase = SigningSessionPhase.NonceContributionsReceived;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public generateAggregatedNonce(): Uint8Array {
|
|
131
|
+
if(this.phase !== SigningSessionPhase.NonceContributionsReceived) {
|
|
132
|
+
throw new SigningSessionError(
|
|
133
|
+
`Cannot aggregate nonces: phase is ${this.phase}, expected NonceContributionsReceived.`,
|
|
134
|
+
'INVALID_PHASE'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.aggregatedNonce = musig2.nonceAggregate([...this.nonceContributions.values()]);
|
|
138
|
+
this.phase = SigningSessionPhase.AwaitingPartialSignatures;
|
|
139
|
+
return this.aggregatedNonce;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public addPartialSignature(participantDid: string, partialSig: Uint8Array): void {
|
|
143
|
+
if(this.phase !== SigningSessionPhase.AwaitingPartialSignatures) {
|
|
144
|
+
throw new SigningSessionError(
|
|
145
|
+
`Partial signatures not expected. Current phase: ${this.phase}`,
|
|
146
|
+
'INVALID_PHASE'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if(!this.cohort.participants.includes(participantDid)) {
|
|
150
|
+
throw new SigningSessionError(
|
|
151
|
+
`Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
|
|
152
|
+
'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if(this.partialSignatures.has(participantDid)) {
|
|
156
|
+
throw new SigningSessionError(
|
|
157
|
+
`Duplicate partial signature from ${participantDid}.`,
|
|
158
|
+
'DUPLICATE_PARTIAL_SIG'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
this.partialSignatures.set(participantDid, partialSig);
|
|
162
|
+
|
|
163
|
+
if(this.partialSignatures.size === this.cohort.participants.length) {
|
|
164
|
+
this.phase = SigningSessionPhase.PartialSignaturesReceived;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public generateFinalSignature(): Uint8Array {
|
|
169
|
+
if(this.phase !== SigningSessionPhase.PartialSignaturesReceived) {
|
|
170
|
+
throw new SigningSessionError(
|
|
171
|
+
`Cannot generate final signature: phase is ${this.phase}.`,
|
|
172
|
+
'INVALID_PHASE'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if(!this.aggregatedNonce) {
|
|
176
|
+
throw new SigningSessionError('Aggregated nonce missing.', 'MISSING_AGGREGATED_NONCE');
|
|
177
|
+
}
|
|
178
|
+
const session = new musig2.Session(
|
|
179
|
+
this.aggregatedNonce,
|
|
180
|
+
this.cohort.cohortKeys,
|
|
181
|
+
this.sigHash,
|
|
182
|
+
[this.cohort.tapTweak],
|
|
183
|
+
[true]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Pre-verify each partial signature against the signer's public key before
|
|
187
|
+
// aggregating (BIP-327 §2.3.5). Delegating verification to partialSigAgg
|
|
188
|
+
// alone makes it impossible to attribute a bad contribution; pinpointing
|
|
189
|
+
// the offending participant lets the service blame and retry without the
|
|
190
|
+
// whole cohort.
|
|
191
|
+
//
|
|
192
|
+
// partialSigVerify(partialSig, pubNonces, i) needs pubNonces ordered to
|
|
193
|
+
// match cohort.cohortKeys, and `i` is the signer's position in that array.
|
|
194
|
+
const pubNoncesByIndex: Uint8Array[] = new Array(this.cohort.cohortKeys.length);
|
|
195
|
+
for(const [did, nonce] of this.nonceContributions) {
|
|
196
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
197
|
+
if(idx < 0) {
|
|
198
|
+
throw new SigningSessionError(
|
|
199
|
+
`Cannot verify nonce from ${did}: participant key missing from cohort.`,
|
|
200
|
+
'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
pubNoncesByIndex[idx] = nonce;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for(const [did, partialSig] of this.partialSignatures) {
|
|
207
|
+
const idx = this.cohort.indexOfParticipant(did);
|
|
208
|
+
if(idx < 0) {
|
|
209
|
+
throw new SigningSessionError(
|
|
210
|
+
`Cannot verify partial signature from ${did}: participant key missing from cohort.`,
|
|
211
|
+
'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
|
|
215
|
+
if(!ok) {
|
|
216
|
+
throw new SigningSessionError(
|
|
217
|
+
`Bad partial signature from ${did}.`,
|
|
218
|
+
'BAD_PARTIAL_SIG', { participantDid: did, index: idx }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
|
|
224
|
+
this.phase = SigningSessionPhase.Complete;
|
|
225
|
+
return this.signature;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Generates a fresh MuSig2 nonce contribution for the participant.
|
|
230
|
+
* Stores the secret nonce internally for use in `generatePartialSignature()`.
|
|
231
|
+
*/
|
|
232
|
+
public generateNonceContribution(participantPublicKey: Uint8Array, participantSecretKey: Uint8Array): Uint8Array {
|
|
233
|
+
const aggPublicKey = musig2.keyAggExport(musig2.keyAggregate(this.cohort.cohortKeys));
|
|
234
|
+
const nonces = musig2.nonceGen(participantPublicKey, participantSecretKey, aggPublicKey);
|
|
235
|
+
this.#secretNonce = nonces.secret;
|
|
236
|
+
return nonces.public;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generates a partial signature using the participant's secret key + secret nonce.
|
|
241
|
+
* Requires the aggregated nonce to have been set first (via the service).
|
|
242
|
+
*
|
|
243
|
+
* Clears the stored secret nonce after use on every path (success or throw)
|
|
244
|
+
* via {@link clearSecrets}. JS cannot truly erase memory (GC may relocate
|
|
245
|
+
* buffers), but overwriting the bytes shortens the exposure window and
|
|
246
|
+
* prevents accidental reuse or serialization of a spent nonce - reuse of a
|
|
247
|
+
* MuSig2 nonce leaks the secret key.
|
|
248
|
+
*/
|
|
249
|
+
public generatePartialSignature(participantSecretKey: Uint8Array): Uint8Array {
|
|
250
|
+
if(!this.aggregatedNonce) {
|
|
251
|
+
throw new SigningSessionError('Aggregated nonce not available.', 'MISSING_AGGREGATED_NONCE');
|
|
252
|
+
}
|
|
253
|
+
if(!this.#secretNonce) {
|
|
254
|
+
throw new SigningSessionError('Secret nonce not available - generateNonceContribution() must be called first.', 'MISSING_SECRET_NONCE');
|
|
255
|
+
}
|
|
256
|
+
const session = new musig2.Session(
|
|
257
|
+
this.aggregatedNonce,
|
|
258
|
+
this.cohort.cohortKeys,
|
|
259
|
+
this.sigHash,
|
|
260
|
+
[this.cohort.tapTweak],
|
|
261
|
+
[true]
|
|
262
|
+
);
|
|
263
|
+
try {
|
|
264
|
+
return session.sign(this.#secretNonce, participantSecretKey);
|
|
265
|
+
} finally {
|
|
266
|
+
this.clearSecrets();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Zeroize any retained secret nonce. Safe to call repeatedly and on any path
|
|
272
|
+
* (completion, failure, or teardown of an abandoned session). Callers that
|
|
273
|
+
* drop a session before it reaches a partial signature should invoke this so
|
|
274
|
+
* the secret nonce does not linger on the live object.
|
|
275
|
+
*/
|
|
276
|
+
public clearSecrets(): void {
|
|
277
|
+
if(this.#secretNonce) {
|
|
278
|
+
wipe(this.#secretNonce);
|
|
279
|
+
this.#secretNonce = undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public isComplete(): boolean {
|
|
284
|
+
return this.phase === SigningSessionPhase.Complete;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public isFailed(): boolean {
|
|
288
|
+
return this.phase === SigningSessionPhase.Failed;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NotImplementedError } from '@did-btcr2/common';
|
|
2
|
+
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
3
|
+
import type { BaseMessage } from '../messages/base.js';
|
|
4
|
+
import type { MessageHandler, Transport } from './transport.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DIDComm Transport (stub).
|
|
8
|
+
*
|
|
9
|
+
* @class DidCommTransport
|
|
10
|
+
* @implements {Transport}
|
|
11
|
+
*/
|
|
12
|
+
export class DidCommTransport implements Transport {
|
|
13
|
+
public name: string = 'didcomm';
|
|
14
|
+
|
|
15
|
+
public start(): void {
|
|
16
|
+
throw new NotImplementedError('DidCommTransport not implemented. Use NostrTransport instead.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public registerActor(_did: string, _keys: SchnorrKeyPair): void {
|
|
20
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public getActorPk(_did: string): Uint8Array | undefined {
|
|
24
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public registerPeer(_did: string, _communicationPk: Uint8Array): void {
|
|
28
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public getPeerPk(_did: string): Uint8Array | undefined {
|
|
32
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public registerMessageHandler(_actorDid: string, _messageType: string, _handler: MessageHandler): void {
|
|
36
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public unregisterMessageHandler(_actorDid: string, _messageType: string): void {
|
|
40
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public unregisterActor(_did: string): void {
|
|
44
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async sendMessage(_message: BaseMessage, _sender: string, _recipient?: string): Promise<void> {
|
|
48
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public publishRepeating(
|
|
52
|
+
_message: BaseMessage,
|
|
53
|
+
_sender: string,
|
|
54
|
+
_intervalMs: number,
|
|
55
|
+
_recipient?: string,
|
|
56
|
+
): () => void {
|
|
57
|
+
throw new NotImplementedError('DidCommTransport not implemented.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MethodError } from '@did-btcr2/common';
|
|
2
|
+
|
|
3
|
+
export class TransportError extends MethodError {
|
|
4
|
+
constructor(message: string, type: string = 'TransportError', data?: Record<string, any>) {
|
|
5
|
+
super(message, type, data);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class TransportAdapterError extends MethodError {
|
|
10
|
+
constructor(message: string, type: string = 'TransportAdapterError', data?: Record<string, any>) {
|
|
11
|
+
super(message, type, data);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NotImplementedError } from '@did-btcr2/common';
|
|
2
|
+
import type { Logger } from '../logger.js';
|
|
3
|
+
import { TransportError } from './error.js';
|
|
4
|
+
import type { HttpClientTransportConfig } from './http/client.js';
|
|
5
|
+
import { HttpClientTransport } from './http/client.js';
|
|
6
|
+
import type { HttpServerTransportConfig } from './http/server.js';
|
|
7
|
+
import { HttpServerTransport } from './http/server.js';
|
|
8
|
+
import { NostrTransport } from './nostr.js';
|
|
9
|
+
import type { Transport } from './transport.js';
|
|
10
|
+
|
|
11
|
+
/** Discriminated-union config for {@link TransportFactory.establish}. */
|
|
12
|
+
export type TransportConfig =
|
|
13
|
+
| NostrTransportConfigOption
|
|
14
|
+
| DidCommTransportConfigOption
|
|
15
|
+
| HttpClientTransportConfigOption
|
|
16
|
+
| HttpServerTransportConfigOption;
|
|
17
|
+
|
|
18
|
+
export interface NostrTransportConfigOption {
|
|
19
|
+
type: 'nostr';
|
|
20
|
+
relays?: string[];
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
broadcastLookbackMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DidCommTransportConfigOption {
|
|
26
|
+
type: 'didcomm';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HttpClientTransportConfigOption extends HttpClientTransportConfig {
|
|
30
|
+
type: 'http';
|
|
31
|
+
role: 'client';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HttpServerTransportConfigOption extends HttpServerTransportConfig {
|
|
35
|
+
type: 'http';
|
|
36
|
+
role: 'server';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Factory for creating Transport instances. */
|
|
40
|
+
export class TransportFactory {
|
|
41
|
+
static establish(config: TransportConfig): Transport {
|
|
42
|
+
switch(config.type) {
|
|
43
|
+
case 'nostr':
|
|
44
|
+
return new NostrTransport({
|
|
45
|
+
relays : config.relays,
|
|
46
|
+
logger : config.logger,
|
|
47
|
+
broadcastLookbackMs : config.broadcastLookbackMs,
|
|
48
|
+
});
|
|
49
|
+
case 'didcomm':
|
|
50
|
+
throw new NotImplementedError('DIDComm transport not implemented yet.');
|
|
51
|
+
case 'http':
|
|
52
|
+
if(config.role === 'client') return new HttpClientTransport(config);
|
|
53
|
+
if(config.role === 'server') return new HttpServerTransport(config);
|
|
54
|
+
throw new NotImplementedError(
|
|
55
|
+
`HTTP transport role not implemented: ${(config as { role: string }).role}`,
|
|
56
|
+
);
|
|
57
|
+
default:
|
|
58
|
+
throw new TransportError(
|
|
59
|
+
`Invalid transport type: ${(config as { type: string }).type}`,
|
|
60
|
+
'INVALID_TRANSPORT_TYPE',
|
|
61
|
+
{ config },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|