@did-btcr2/method 0.27.0 → 0.29.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.
Files changed (114) hide show
  1. package/README.md +38 -9
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +20181 -31588
  4. package/dist/browser.mjs +20110 -31517
  5. package/dist/cjs/index.js +1355 -422
  6. package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
  8. package/dist/esm/core/aggregation/cohort.js +31 -8
  9. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  10. package/dist/esm/core/aggregation/logger.js +15 -0
  11. package/dist/esm/core/aggregation/logger.js.map +1 -0
  12. package/dist/esm/core/aggregation/messages/base.js +12 -1
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +90 -0
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
  16. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  17. package/dist/esm/core/aggregation/messages/index.js +1 -0
  18. package/dist/esm/core/aggregation/messages/index.js.map +1 -1
  19. package/dist/esm/core/aggregation/participant.js +39 -46
  20. package/dist/esm/core/aggregation/participant.js.map +1 -1
  21. package/dist/esm/core/aggregation/runner/participant-runner.js +34 -4
  22. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  23. package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
  24. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  25. package/dist/esm/core/aggregation/service.js +143 -15
  26. package/dist/esm/core/aggregation/service.js.map +1 -1
  27. package/dist/esm/core/aggregation/signing-session.js +44 -5
  28. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  29. package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
  30. package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
  31. package/dist/esm/core/aggregation/transport/nostr.js +245 -16
  32. package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
  33. package/dist/esm/core/beacon/beacon.js +147 -61
  34. package/dist/esm/core/beacon/beacon.js.map +1 -1
  35. package/dist/esm/core/beacon/utils.js +14 -9
  36. package/dist/esm/core/beacon/utils.js.map +1 -1
  37. package/dist/esm/core/updater.js +269 -0
  38. package/dist/esm/core/updater.js.map +1 -0
  39. package/dist/esm/did-btcr2.js +30 -46
  40. package/dist/esm/did-btcr2.js.map +1 -1
  41. package/dist/esm/index.js +4 -1
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/utils/did-document.js +2 -2
  44. package/dist/esm/utils/did-document.js.map +1 -1
  45. package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
  46. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
  47. package/dist/types/core/aggregation/cohort.d.ts +20 -3
  48. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  49. package/dist/types/core/aggregation/logger.d.ts +22 -0
  50. package/dist/types/core/aggregation/logger.d.ts.map +1 -0
  51. package/dist/types/core/aggregation/messages/base.d.ts +13 -1
  52. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  53. package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
  54. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
  55. package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
  56. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  57. package/dist/types/core/aggregation/messages/index.d.ts +1 -0
  58. package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
  59. package/dist/types/core/aggregation/participant.d.ts +2 -0
  60. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  61. package/dist/types/core/aggregation/runner/events.d.ts +32 -6
  62. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  63. package/dist/types/core/aggregation/runner/participant-runner.d.ts +8 -2
  64. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  65. package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
  66. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  67. package/dist/types/core/aggregation/service.d.ts +33 -2
  68. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  69. package/dist/types/core/aggregation/signing-session.d.ts +5 -1
  70. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  71. package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
  72. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
  73. package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
  74. package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
  75. package/dist/types/core/aggregation/transport/transport.d.ts +25 -0
  76. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  77. package/dist/types/core/beacon/beacon.d.ts +85 -18
  78. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  79. package/dist/types/core/beacon/utils.d.ts +2 -2
  80. package/dist/types/core/beacon/utils.d.ts.map +1 -1
  81. package/dist/types/core/updater.d.ts +178 -0
  82. package/dist/types/core/updater.d.ts.map +1 -0
  83. package/dist/types/did-btcr2.d.ts +23 -23
  84. package/dist/types/did-btcr2.d.ts.map +1 -1
  85. package/dist/types/index.d.ts +4 -1
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/package.json +4 -6
  88. package/src/core/aggregation/beacon-strategy.ts +123 -0
  89. package/src/core/aggregation/cohort.ts +34 -8
  90. package/src/core/aggregation/logger.ts +33 -0
  91. package/src/core/aggregation/messages/base.ts +20 -5
  92. package/src/core/aggregation/messages/bodies.ts +223 -0
  93. package/src/core/aggregation/messages/factories.ts +1 -0
  94. package/src/core/aggregation/messages/index.ts +1 -0
  95. package/src/core/aggregation/participant.ts +40 -46
  96. package/src/core/aggregation/runner/events.ts +27 -3
  97. package/src/core/aggregation/runner/participant-runner.ts +42 -4
  98. package/src/core/aggregation/runner/service-runner.ts +227 -19
  99. package/src/core/aggregation/service.ts +189 -20
  100. package/src/core/aggregation/signing-session.ts +65 -7
  101. package/src/core/aggregation/transport/didcomm.ts +17 -0
  102. package/src/core/aggregation/transport/nostr.ts +266 -23
  103. package/src/core/aggregation/transport/transport.ts +33 -0
  104. package/src/core/beacon/beacon.ts +217 -76
  105. package/src/core/beacon/utils.ts +16 -11
  106. package/src/core/updater.ts +415 -0
  107. package/src/did-btcr2.ts +36 -71
  108. package/src/index.ts +4 -1
  109. package/src/utils/did-document.ts +2 -2
  110. package/dist/esm/core/update.js +0 -112
  111. package/dist/esm/core/update.js.map +0 -1
  112. package/dist/types/core/update.d.ts +0 -52
  113. package/dist/types/core/update.d.ts.map +0 -1
  114. package/src/core/update.ts +0 -158
@@ -2,9 +2,10 @@ import { canonicalHash, canonicalize, hash } from '@did-btcr2/common';
2
2
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
3
3
  import type { SerializedSMTProof, TreeEntry } from '@did-btcr2/smt';
4
4
  import { BTCR2MerkleTree } from '@did-btcr2/smt';
5
+ import { schnorr } from '@noble/curves/secp256k1.js';
5
6
  import { hexToBytes, randomBytes } from '@noble/hashes/utils';
7
+ import { p2tr } from '@scure/btc-signer';
6
8
  import { keyAggExport, keyAggregate, sortKeys } from '@scure/btc-signer/musig2';
7
- import { crypto as btcCrypto, payments } from 'bitcoinjs-lib';
8
9
  import type { CASAnnouncement } from '../types.js';
9
10
  import { AggregationCohortError } from './errors.js';
10
11
 
@@ -48,11 +49,23 @@ export class AggregationCohort {
48
49
  /** List of participant DIDs that have been accepted into the cohort. */
49
50
  participants: Array<string> = [];
50
51
 
52
+ /**
53
+ * Mapping from participant DID → their compressed secp256k1 public key.
54
+ * Distinct from {@link cohortKeys} (which is sorted per BIP-327) — this lets
55
+ * callers look up a participant's key without knowing their position in the
56
+ * sorted array. Populated by the service at `acceptParticipant` time.
57
+ */
58
+ participantKeys: Map<string, Uint8Array> = new Map();
59
+
51
60
  /** Sorted list of cohort participants' compressed public keys. */
52
61
  #cohortKeys: Array<Uint8Array> = [];
53
62
 
54
- /** Taproot tweak (BIP-341 key-path-only). */
55
- trMerkleRoot: Uint8Array = new Uint8Array();
63
+ /**
64
+ * BIP-341 TapTweak `taggedHash("TapTweak", internalPubkey)` for a key-path-only
65
+ * Taproot output. Despite prior naming, this is NOT a Merkle root: key-path-only
66
+ * spends have no script tree.
67
+ */
68
+ tapTweak: Uint8Array = new Uint8Array();
56
69
 
57
70
  /** The n-of-n MuSig2 Taproot beacon address. */
58
71
  beaconAddress: string = '';
@@ -94,7 +107,7 @@ export class AggregationCohort {
94
107
 
95
108
  /**
96
109
  * Computes the n-of-n MuSig2 Taproot beacon address from cohort keys.
97
- * Sets `trMerkleRoot` to the BIP-341 key-path-only tweak.
110
+ * Sets `tapTweak` to the BIP-341 key-path-only tweak.
98
111
  */
99
112
  public computeBeaconAddress(): string {
100
113
  if(this.#cohortKeys.length === 0) {
@@ -105,11 +118,11 @@ export class AggregationCohort {
105
118
  }
106
119
  const keyAggContext = keyAggregate(this.#cohortKeys);
107
120
  const aggPubkey = keyAggExport(keyAggContext);
108
- const payment = payments.p2tr({ internalPubkey: aggPubkey });
121
+ const payment = p2tr(aggPubkey);
109
122
 
110
- // BIP-341: key-path-only P2TR has no script tree, so payment.hash is null.
111
- // Compute the tweak: taggedHash("TapTweak", internalPubkey).
112
- this.trMerkleRoot = payment.hash ?? btcCrypto.taggedHash('TapTweak', aggPubkey);
123
+ // BIP-341: key-path-only P2TR has no script tree. Compute the tweak:
124
+ // taggedHash("TapTweak", internalPubkey).
125
+ this.tapTweak = schnorr.utils.taggedHash('TapTweak', aggPubkey);
113
126
 
114
127
  if(!payment.address) {
115
128
  throw new AggregationCohortError(
@@ -147,6 +160,19 @@ export class AggregationCohort {
147
160
  }
148
161
  }
149
162
 
163
+ /**
164
+ * Returns the position of a participant's public key in the sorted
165
+ * {@link cohortKeys} array, or -1 if the participant is not in the cohort.
166
+ * Required by MuSig2 partial-sig verification which indexes by signer position.
167
+ */
168
+ public indexOfParticipant(did: string): number {
169
+ const pk = this.participantKeys.get(did);
170
+ if(!pk) return -1;
171
+ return this.#cohortKeys.findIndex(k =>
172
+ k.length === pk.length && k.every((b, i) => b === pk[i])
173
+ );
174
+ }
175
+
150
176
  public addUpdate(participantDid: string, signedUpdate: SignedBTCR2Update): void {
151
177
  if(!this.participants.includes(participantDid)) {
152
178
  throw new AggregationCohortError(
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Minimal injectable logger for the aggregation subsystem.
3
+ *
4
+ * Each runner and transport adapter accepts a `Logger` option; the default is
5
+ * {@link CONSOLE_LOGGER}, which forwards to `console.*`. Pass
6
+ * {@link SILENT_LOGGER} to suppress output (useful for tests) or a custom
7
+ * implementation to route logs to pino, winston, Sentry, etc.
8
+ *
9
+ * The interface is intentionally small — we don't want production code taking
10
+ * a hard dependency on any specific logger library.
11
+ */
12
+ export interface Logger {
13
+ debug(message: string, ...args: unknown[]): void;
14
+ info(message: string, ...args: unknown[]): void;
15
+ warn(message: string, ...args: unknown[]): void;
16
+ error(message: string, ...args: unknown[]): void;
17
+ }
18
+
19
+ /** Console-backed logger. Default for runners and transports. */
20
+ export const CONSOLE_LOGGER: Logger = {
21
+ debug : (msg, ...args) => console.debug(msg, ...args),
22
+ info : (msg, ...args) => console.info(msg, ...args),
23
+ warn : (msg, ...args) => console.warn(msg, ...args),
24
+ error : (msg, ...args) => console.error(msg, ...args),
25
+ };
26
+
27
+ /** No-op logger. Useful for tests and production environments with own logging pipeline. */
28
+ export const SILENT_LOGGER: Logger = {
29
+ debug : () => {},
30
+ info : () => {},
31
+ warn : () => {},
32
+ error : () => {},
33
+ };
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Current on-the-wire protocol version.
3
+ *
4
+ * Receivers reject messages with an unknown (mismatched) version. Bumping this
5
+ * requires coordinated updates across all participants and any intermediate
6
+ * relays that inspect message content.
7
+ */
8
+ export const AGGREGATION_WIRE_VERSION = 1;
9
+
1
10
  export type BaseBody = {
2
11
  cohortId: string;
3
12
  cohortSize?: number;
@@ -10,6 +19,8 @@ export type BaseBody = {
10
19
  nonceContribution?: Uint8Array;
11
20
  partialSignature?: Uint8Array;
12
21
  pendingTx?: string;
22
+ /** Hex-encoded scriptPubKey of the UTXO being spent. Required for BIP-341 sighash. */
23
+ prevOutScriptHex?: string;
13
24
  prevOutValue?: string;
14
25
  communicationPk?: Uint8Array;
15
26
  beaconType?: string;
@@ -23,6 +34,7 @@ export type BaseBody = {
23
34
 
24
35
  export type Base = {
25
36
  type: string;
37
+ version?: number;
26
38
  to?: string;
27
39
  from: string;
28
40
  body?: BaseBody;
@@ -30,12 +42,14 @@ export type Base = {
30
42
 
31
43
  export class BaseMessage {
32
44
  public type: string;
45
+ public version: number;
33
46
  public to?: string;
34
47
  public from: string;
35
48
  public body?: BaseBody;
36
49
 
37
- constructor({ type, to, from, body }: Base) {
50
+ constructor({ type, version, to, from, body }: Base) {
38
51
  this.type = type;
52
+ this.version = version ?? AGGREGATION_WIRE_VERSION;
39
53
  this.to = to;
40
54
  this.from = from;
41
55
  this.body = body;
@@ -47,10 +61,11 @@ export class BaseMessage {
47
61
  */
48
62
  public toJSON(): Base {
49
63
  return {
50
- type : this.type,
51
- to : this.to,
52
- from : this.from,
53
- body : this.body
64
+ type : this.type,
65
+ version : this.version,
66
+ to : this.to,
67
+ from : this.from,
68
+ body : this.body
54
69
  };
55
70
  }
56
71
  }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Per-message-type body interfaces and a discriminated {@link AggregationMessage}
3
+ * union.
4
+ *
5
+ * {@link BaseBody} remains the superset-of-all-fields body type used by the
6
+ * raw {@link BaseMessage} class (see `base.ts`). The narrow interfaces here
7
+ * describe what each specific message type is *required* to carry and are
8
+ * exposed alongside type guards for consumers who want compile-time narrowing.
9
+ *
10
+ * Guards validate both `type` and the presence of required body fields so they
11
+ * are safe to use on messages that have round-tripped through JSON / a relay.
12
+ */
13
+
14
+ import type { SerializedSMTProof } from '@did-btcr2/smt';
15
+ import type { BaseMessage } from './base.js';
16
+ import {
17
+ AGGREGATED_NONCE,
18
+ AUTHORIZATION_REQUEST,
19
+ COHORT_ADVERT,
20
+ COHORT_OPT_IN,
21
+ COHORT_OPT_IN_ACCEPT,
22
+ COHORT_READY,
23
+ DISTRIBUTE_AGGREGATED_DATA,
24
+ NONCE_CONTRIBUTION,
25
+ SIGNATURE_AUTHORIZATION,
26
+ SUBMIT_UPDATE,
27
+ VALIDATION_ACK,
28
+ } from './constants.js';
29
+
30
+ // ── Cohort formation (Step 1) ─────────────────────────────────────────────
31
+
32
+ export interface CohortAdvertBody {
33
+ cohortId: string;
34
+ cohortSize: number;
35
+ beaconType: string;
36
+ network: string;
37
+ communicationPk: Uint8Array;
38
+ }
39
+
40
+ export interface CohortOptInBody {
41
+ cohortId: string;
42
+ participantPk: Uint8Array;
43
+ communicationPk: Uint8Array;
44
+ }
45
+
46
+ export interface CohortOptInAcceptBody {
47
+ cohortId: string;
48
+ }
49
+
50
+ export interface CohortReadyBody {
51
+ cohortId: string;
52
+ beaconAddress: string;
53
+ cohortKeys: Array<Uint8Array>;
54
+ }
55
+
56
+ // ── Update / aggregation (Steps 2-3) ──────────────────────────────────────
57
+
58
+ export interface SubmitUpdateBody {
59
+ cohortId: string;
60
+ signedUpdate: Record<string, unknown>;
61
+ }
62
+
63
+ export interface DistributeAggregatedDataBody {
64
+ cohortId: string;
65
+ beaconType: string;
66
+ signalBytesHex: string;
67
+ casAnnouncement?: Record<string, string>;
68
+ smtProof?: Record<string, unknown> | SerializedSMTProof;
69
+ }
70
+
71
+ export interface ValidationAckBody {
72
+ cohortId: string;
73
+ approved: boolean;
74
+ }
75
+
76
+ // ── Signing (Step 4) ──────────────────────────────────────────────────────
77
+
78
+ export interface AuthorizationRequestBody {
79
+ cohortId: string;
80
+ sessionId: string;
81
+ pendingTx: string;
82
+ prevOutScriptHex: string;
83
+ prevOutValue: string;
84
+ }
85
+
86
+ export interface NonceContributionBody {
87
+ cohortId: string;
88
+ sessionId: string;
89
+ nonceContribution: Uint8Array;
90
+ }
91
+
92
+ export interface AggregatedNonceBody {
93
+ cohortId: string;
94
+ sessionId: string;
95
+ aggregatedNonce: Uint8Array;
96
+ }
97
+
98
+ export interface SignatureAuthorizationBody {
99
+ cohortId: string;
100
+ sessionId: string;
101
+ partialSignature: Uint8Array;
102
+ }
103
+
104
+ // ── Narrow message types (BaseMessage & { type, body }) ──────────────────
105
+
106
+ export type CohortAdvertMessage = BaseMessage & { type: typeof COHORT_ADVERT; body: CohortAdvertBody };
107
+ export type CohortOptInMessage = BaseMessage & { type: typeof COHORT_OPT_IN; body: CohortOptInBody };
108
+ export type CohortOptInAcceptMessage = BaseMessage & { type: typeof COHORT_OPT_IN_ACCEPT; body: CohortOptInAcceptBody };
109
+ export type CohortReadyMessage = BaseMessage & { type: typeof COHORT_READY; body: CohortReadyBody };
110
+ export type SubmitUpdateMessage = BaseMessage & { type: typeof SUBMIT_UPDATE; body: SubmitUpdateBody };
111
+ export type DistributeAggregatedDataMessage = BaseMessage & { type: typeof DISTRIBUTE_AGGREGATED_DATA; body: DistributeAggregatedDataBody };
112
+ export type ValidationAckMessage = BaseMessage & { type: typeof VALIDATION_ACK; body: ValidationAckBody };
113
+ export type AuthorizationRequestMessage = BaseMessage & { type: typeof AUTHORIZATION_REQUEST; body: AuthorizationRequestBody };
114
+ export type NonceContributionMessage = BaseMessage & { type: typeof NONCE_CONTRIBUTION; body: NonceContributionBody };
115
+ export type AggregatedNonceMessage = BaseMessage & { type: typeof AGGREGATED_NONCE; body: AggregatedNonceBody };
116
+ export type SignatureAuthorizationMessage = BaseMessage & { type: typeof SIGNATURE_AUTHORIZATION; body: SignatureAuthorizationBody };
117
+
118
+ /** Discriminated union of every well-formed aggregation message. */
119
+ export type AggregationMessage =
120
+ | CohortAdvertMessage
121
+ | CohortOptInMessage
122
+ | CohortOptInAcceptMessage
123
+ | CohortReadyMessage
124
+ | SubmitUpdateMessage
125
+ | DistributeAggregatedDataMessage
126
+ | ValidationAckMessage
127
+ | AuthorizationRequestMessage
128
+ | NonceContributionMessage
129
+ | AggregatedNonceMessage
130
+ | SignatureAuthorizationMessage;
131
+
132
+ // ── Type guards ───────────────────────────────────────────────────────────
133
+ // Each guard validates `type` plus required body fields so it's safe to use
134
+ // on messages that have round-tripped through JSON / a relay.
135
+
136
+ const hasStr = (b: unknown, k: string): boolean =>
137
+ !!b && typeof (b as Record<string, unknown>)[k] === 'string';
138
+ const hasNum = (b: unknown, k: string): boolean =>
139
+ !!b && typeof (b as Record<string, unknown>)[k] === 'number';
140
+ const hasBool = (b: unknown, k: string): boolean =>
141
+ !!b && typeof (b as Record<string, unknown>)[k] === 'boolean';
142
+ const hasBytes = (b: unknown, k: string): boolean =>
143
+ !!b && (b as Record<string, unknown>)[k] instanceof Uint8Array;
144
+ const hasBytesArray = (b: unknown, k: string): boolean => {
145
+ const v = b ? (b as Record<string, unknown>)[k] : undefined;
146
+ return Array.isArray(v) && v.every(x => x instanceof Uint8Array);
147
+ };
148
+
149
+ export function isCohortAdvertMessage(m: BaseMessage): m is CohortAdvertMessage {
150
+ return m.type === COHORT_ADVERT
151
+ && hasStr(m.body, 'cohortId')
152
+ && hasNum(m.body, 'cohortSize')
153
+ && hasStr(m.body, 'beaconType')
154
+ && hasStr(m.body, 'network')
155
+ && hasBytes(m.body, 'communicationPk');
156
+ }
157
+
158
+ export function isCohortOptInMessage(m: BaseMessage): m is CohortOptInMessage {
159
+ return m.type === COHORT_OPT_IN
160
+ && hasStr(m.body, 'cohortId')
161
+ && hasBytes(m.body, 'participantPk')
162
+ && hasBytes(m.body, 'communicationPk');
163
+ }
164
+
165
+ export function isCohortOptInAcceptMessage(m: BaseMessage): m is CohortOptInAcceptMessage {
166
+ return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, 'cohortId');
167
+ }
168
+
169
+ export function isCohortReadyMessage(m: BaseMessage): m is CohortReadyMessage {
170
+ return m.type === COHORT_READY
171
+ && hasStr(m.body, 'cohortId')
172
+ && hasStr(m.body, 'beaconAddress')
173
+ && hasBytesArray(m.body, 'cohortKeys');
174
+ }
175
+
176
+ export function isSubmitUpdateMessage(m: BaseMessage): m is SubmitUpdateMessage {
177
+ return m.type === SUBMIT_UPDATE
178
+ && hasStr(m.body, 'cohortId')
179
+ && !!m.body && typeof (m.body as Record<string, unknown>).signedUpdate === 'object';
180
+ }
181
+
182
+ export function isDistributeAggregatedDataMessage(m: BaseMessage): m is DistributeAggregatedDataMessage {
183
+ return m.type === DISTRIBUTE_AGGREGATED_DATA
184
+ && hasStr(m.body, 'cohortId')
185
+ && hasStr(m.body, 'beaconType')
186
+ && hasStr(m.body, 'signalBytesHex');
187
+ }
188
+
189
+ export function isValidationAckMessage(m: BaseMessage): m is ValidationAckMessage {
190
+ return m.type === VALIDATION_ACK
191
+ && hasStr(m.body, 'cohortId')
192
+ && hasBool(m.body, 'approved');
193
+ }
194
+
195
+ export function isAuthorizationRequestMessage(m: BaseMessage): m is AuthorizationRequestMessage {
196
+ return m.type === AUTHORIZATION_REQUEST
197
+ && hasStr(m.body, 'cohortId')
198
+ && hasStr(m.body, 'sessionId')
199
+ && hasStr(m.body, 'pendingTx')
200
+ && hasStr(m.body, 'prevOutScriptHex')
201
+ && hasStr(m.body, 'prevOutValue');
202
+ }
203
+
204
+ export function isNonceContributionMessage(m: BaseMessage): m is NonceContributionMessage {
205
+ return m.type === NONCE_CONTRIBUTION
206
+ && hasStr(m.body, 'cohortId')
207
+ && hasStr(m.body, 'sessionId')
208
+ && hasBytes(m.body, 'nonceContribution');
209
+ }
210
+
211
+ export function isAggregatedNonceMessage(m: BaseMessage): m is AggregatedNonceMessage {
212
+ return m.type === AGGREGATED_NONCE
213
+ && hasStr(m.body, 'cohortId')
214
+ && hasStr(m.body, 'sessionId')
215
+ && hasBytes(m.body, 'aggregatedNonce');
216
+ }
217
+
218
+ export function isSignatureAuthorizationMessage(m: BaseMessage): m is SignatureAuthorizationMessage {
219
+ return m.type === SIGNATURE_AUTHORIZATION
220
+ && hasStr(m.body, 'cohortId')
221
+ && hasStr(m.body, 'sessionId')
222
+ && hasBytes(m.body, 'partialSignature');
223
+ }
@@ -164,6 +164,7 @@ type AuthorizationRequestMessage = {
164
164
  cohortId: string;
165
165
  sessionId: string;
166
166
  pendingTx: string;
167
+ prevOutScriptHex: string;
167
168
  prevOutValue: string;
168
169
  };
169
170
  type NonceContributionMessage = {
@@ -1,4 +1,5 @@
1
1
  export * from './base.js';
2
+ export * from './bodies.js';
2
3
  export * from './constants.js';
3
4
  export * from './factories.js';
4
5
  export * from './guards.js';
@@ -1,13 +1,14 @@
1
- import { canonicalHash, canonicalize } from '@did-btcr2/common';
1
+ import { canonicalHash } from '@did-btcr2/common';
2
2
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
3
3
  import type { SchnorrKeyPair } from '@did-btcr2/keypair';
4
4
  import type { SerializedSMTProof} from '@did-btcr2/smt';
5
- import { blockHash, didToIndex, hashToHex, hexToHash, verifySerializedProof } from '@did-btcr2/smt';
6
- import { bytesToHex } from '@noble/hashes/utils';
7
- import { Transaction } from 'bitcoinjs-lib';
5
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
6
+ import { Transaction } from '@scure/btc-signer';
7
+ import { getBeaconStrategy } from './beacon-strategy.js';
8
8
  import { AggregationCohort } from './cohort.js';
9
9
  import { AggregationParticipantError } from './errors.js';
10
10
  import type { BaseMessage } from './messages/base.js';
11
+ import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
11
12
  import {
12
13
  AGGREGATED_NONCE,
13
14
  AUTHORIZATION_REQUEST,
@@ -61,6 +62,8 @@ export interface PendingSigningRequest {
61
62
  cohortId: string;
62
63
  sessionId: string;
63
64
  pendingTxHex: string;
65
+ /** Hex-encoded scriptPubKey of the UTXO being spent. Required for BIP-341 sighash. */
66
+ prevOutScriptHex: string;
64
67
  prevOutValue: string;
65
68
  }
66
69
 
@@ -110,6 +113,10 @@ export class AggregationParticipant {
110
113
  * outgoing messages — those come exclusively from action methods.
111
114
  */
112
115
  public receive(message: BaseMessage): void {
116
+ // Reject messages whose wire version doesn't match what this build speaks.
117
+ if(message.version === undefined || message.version !== AGGREGATION_WIRE_VERSION) {
118
+ return;
119
+ }
113
120
  const type = message.type;
114
121
  switch(type) {
115
122
  case COHORT_ADVERT:
@@ -291,46 +298,28 @@ export class AggregationParticipant {
291
298
  if(!state.submittedUpdate) return;
292
299
 
293
300
  const beaconType = message.body?.beaconType;
301
+ if(!beaconType) return;
302
+ const strategy = getBeaconStrategy(beaconType);
303
+ if(!strategy) return;
304
+
294
305
  const signalBytesHex = message.body?.signalBytesHex ?? '';
295
306
  const expectedHash = canonicalHash(state.submittedUpdate);
296
- let matches = false;
297
-
298
- if(beaconType === 'CASBeacon') {
299
- const casAnnouncement = message.body?.casAnnouncement;
300
- if(casAnnouncement) {
301
- matches = casAnnouncement[this.did] === expectedHash;
302
- state.validation = {
303
- cohortId,
304
- beaconType,
305
- signalBytesHex,
306
- casAnnouncement,
307
- expectedHash,
308
- matches,
309
- };
310
- }
311
- } else if(beaconType === 'SMTBeacon') {
312
- const smtProof = message.body?.smtProof as unknown as SerializedSMTProof | undefined;
313
- if(smtProof?.updateId && smtProof?.nonce) {
314
- // Verify updateId matches the canonicalized update hash
315
- const canonicalBytes = new TextEncoder().encode(canonicalize(state.submittedUpdate));
316
- const expectedUpdateId = hashToHex(blockHash(canonicalBytes));
317
- if(smtProof.updateId === expectedUpdateId) {
318
- // Verify Merkle inclusion
319
- const index = didToIndex(this.did);
320
- const candidateHash = blockHash(blockHash(hexToHash(smtProof.nonce)), hexToHash(smtProof.updateId));
321
- matches = verifySerializedProof(smtProof, index, candidateHash);
322
- }
323
- state.validation = {
324
- cohortId,
325
- beaconType,
326
- signalBytesHex,
327
- smtProof,
328
- expectedHash,
329
- matches,
330
- };
331
- }
332
- }
307
+ const result = strategy.validateParticipantView({
308
+ participantDid : this.did,
309
+ submittedUpdate : state.submittedUpdate,
310
+ expectedHash,
311
+ body : message.body!,
312
+ });
333
313
 
314
+ state.validation = {
315
+ cohortId,
316
+ beaconType,
317
+ signalBytesHex,
318
+ expectedHash,
319
+ matches : result.matches,
320
+ casAnnouncement : result.casAnnouncement,
321
+ smtProof : result.smtProof,
322
+ };
334
323
  state.phase = ParticipantCohortPhase.AwaitingValidation;
335
324
  }
336
325
 
@@ -395,13 +384,15 @@ export class AggregationParticipant {
395
384
 
396
385
  const sessionId = message.body?.sessionId;
397
386
  const pendingTxHex = message.body?.pendingTx;
387
+ const prevOutScriptHex = message.body?.prevOutScriptHex;
398
388
  const prevOutValue = message.body?.prevOutValue;
399
- if(!sessionId || !pendingTxHex || !prevOutValue) return;
389
+ if(!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
400
390
 
401
391
  state.signingRequest = {
402
392
  cohortId,
403
393
  sessionId,
404
394
  pendingTxHex,
395
+ prevOutScriptHex,
405
396
  prevOutValue,
406
397
  };
407
398
  state.phase = ParticipantCohortPhase.AwaitingSigning;
@@ -425,11 +416,14 @@ export class AggregationParticipant {
425
416
  );
426
417
  }
427
418
 
428
- const tx = Transaction.fromHex(state.signingRequest.pendingTxHex);
419
+ const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
429
420
 
430
- // Derive UTXO metadata for Taproot sighash (BIP-341).
431
- // The beacon TX's change output (index 0) uses the same Taproot address as the input.
432
- const prevOutScripts = tx.outs.length > 0 ? [tx.outs[0].script] : [];
421
+ // Derive UTXO metadata for Taproot sighash (BIP-341). Use the script
422
+ // supplied by the service in AUTHORIZATION_REQUEST rather than reading
423
+ // the change output: input and change may use different scripts in future
424
+ // beacon designs, and the prevOutScript must be the UTXO script, not the
425
+ // change script.
426
+ const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
433
427
  const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
434
428
 
435
429
  const session = new BeaconSigningSession({
@@ -1,5 +1,6 @@
1
+ import type { SerializedSMTProof } from '@did-btcr2/smt';
1
2
  import type { CohortAdvert, PendingSigningRequest, PendingValidation } from '../participant.js';
2
- import type { AggregationResult, PendingOptIn } from '../service.js';
3
+ import type { AggregationResult, PendingOptIn, Rejection } from '../service.js';
3
4
 
4
5
  /**
5
6
  * AggregationServiceRunner events are emitted by the AggregationServiceRunner to signal important
@@ -22,6 +23,13 @@ export type AggregationServiceEvents = {
22
23
  /** A participant has submitted a signed update. */
23
24
  'update-received': [{ participantDid: string }];
24
25
 
26
+ /**
27
+ * An inbound message was silently dropped by the state machine (bad proof,
28
+ * oversized payload, wrong wire version, etc.). Fires for *any* rejection,
29
+ * not just SUBMIT_UPDATE.
30
+ */
31
+ 'message-rejected': [Rejection & { cohortId: string }];
32
+
25
33
  /** Aggregated data has been distributed to all participants for validation. */
26
34
  'data-distributed': [{ cohortId: string }];
27
35
 
@@ -37,6 +45,9 @@ export type AggregationServiceEvents = {
37
45
  /** Signing complete — final aggregated signature is ready to broadcast. */
38
46
  'signing-complete': [AggregationResult];
39
47
 
48
+ /** Cohort transitioned to Failed phase (e.g. a participant rejected validation). */
49
+ 'cohort-failed': [{ cohortId: string; reason: string }];
50
+
40
51
  /** A non-fatal error occurred. Fatal errors reject the run() promise. */
41
52
  'error': [Error];
42
53
  };
@@ -66,8 +77,21 @@ export type AggregationParticipantEvents = {
66
77
  /** Signing request has arrived. Fires before the sign approval callback. */
67
78
  'signing-requested': [PendingSigningRequest];
68
79
 
69
- /** Cohort signing is complete from this participant's perspective. */
70
- 'cohort-complete': [{ cohortId: string; beaconAddress: string }];
80
+ /**
81
+ * Cohort signing is complete from this participant's perspective.
82
+ * Includes the aggregated sidecar data the participant needs to keep for
83
+ * future DID resolution: the CAS Announcement map (for CAS beacons) or the
84
+ * SMT inclusion proof (for SMT beacons).
85
+ */
86
+ 'cohort-complete': [{
87
+ cohortId: string;
88
+ beaconAddress: string;
89
+ beaconType: string;
90
+ /** DID → base64url update hash. Populated only for CAS beacons. */
91
+ casAnnouncement?: Record<string, string>;
92
+ /** Merkle inclusion proof for this participant's slot. Populated only for SMT beacons. */
93
+ smtProof?: SerializedSMTProof;
94
+ }];
71
95
 
72
96
  /** Cohort failed (rejected validation, signing error, etc.). */
73
97
  'cohort-failed': [{ cohortId: string; reason: string }];