@did-btcr2/method 0.32.0 → 0.34.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 (62) hide show
  1. package/README.md +25 -13
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +332 -582
  4. package/dist/browser.mjs +332 -582
  5. package/dist/cjs/index.js +213 -35
  6. package/dist/esm/core/aggregation/beacon-strategy.js +5 -4
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -1
  8. package/dist/esm/core/aggregation/runner/aggregation-runner.js +66 -0
  9. package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -0
  10. package/dist/esm/core/aggregation/runner/index.js +1 -0
  11. package/dist/esm/core/aggregation/runner/index.js.map +1 -1
  12. package/dist/esm/core/aggregation/transport/in-memory.js +146 -0
  13. package/dist/esm/core/aggregation/transport/in-memory.js.map +1 -0
  14. package/dist/esm/core/aggregation/transport/index.js +1 -0
  15. package/dist/esm/core/aggregation/transport/index.js.map +1 -1
  16. package/dist/esm/core/beacon/beacon.js +10 -8
  17. package/dist/esm/core/beacon/beacon.js.map +1 -1
  18. package/dist/esm/core/beacon/cas-beacon.js +4 -4
  19. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  20. package/dist/esm/core/beacon/factory.js +1 -1
  21. package/dist/esm/core/beacon/singleton-beacon.js +4 -4
  22. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  23. package/dist/esm/core/beacon/smt-beacon.js +23 -15
  24. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  25. package/dist/esm/core/resolver.js +7 -4
  26. package/dist/esm/core/resolver.js.map +1 -1
  27. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -1
  28. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +56 -0
  29. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -0
  30. package/dist/types/core/aggregation/runner/index.d.ts +1 -0
  31. package/dist/types/core/aggregation/runner/index.d.ts.map +1 -1
  32. package/dist/types/core/aggregation/transport/in-memory.d.ts +64 -0
  33. package/dist/types/core/aggregation/transport/in-memory.d.ts.map +1 -0
  34. package/dist/types/core/aggregation/transport/index.d.ts +1 -0
  35. package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
  36. package/dist/types/core/beacon/beacon.d.ts +12 -10
  37. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  38. package/dist/types/core/beacon/cas-beacon.d.ts +4 -4
  39. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  40. package/dist/types/core/beacon/factory.d.ts +3 -3
  41. package/dist/types/core/beacon/factory.d.ts.map +1 -1
  42. package/dist/types/core/beacon/singleton-beacon.d.ts +4 -4
  43. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  44. package/dist/types/core/beacon/smt-beacon.d.ts +4 -4
  45. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  46. package/dist/types/core/interfaces.d.ts +14 -11
  47. package/dist/types/core/interfaces.d.ts.map +1 -1
  48. package/dist/types/core/resolver.d.ts +1 -1
  49. package/dist/types/core/resolver.d.ts.map +1 -1
  50. package/package.json +20 -8
  51. package/src/core/aggregation/beacon-strategy.ts +5 -4
  52. package/src/core/aggregation/runner/aggregation-runner.ts +96 -0
  53. package/src/core/aggregation/runner/index.ts +1 -0
  54. package/src/core/aggregation/transport/in-memory.ts +174 -0
  55. package/src/core/aggregation/transport/index.ts +1 -0
  56. package/src/core/beacon/beacon.ts +12 -10
  57. package/src/core/beacon/cas-beacon.ts +4 -4
  58. package/src/core/beacon/factory.ts +3 -3
  59. package/src/core/beacon/singleton-beacon.ts +4 -4
  60. package/src/core/beacon/smt-beacon.ts +24 -16
  61. package/src/core/interfaces.ts +14 -11
  62. package/src/core/resolver.ts +9 -6
@@ -0,0 +1,174 @@
1
+ import type { SchnorrKeyPair } from '@did-btcr2/keypair';
2
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
3
+ import type { BaseMessage } from '../messages/base.js';
4
+ import type { MessageHandler, Transport } from './transport.js';
5
+
6
+ /** Internal registration for a single actor sharing an {@link InMemoryTransport}. */
7
+ interface ActorEntry {
8
+ keys: SchnorrKeyPair;
9
+ handlers: Map<string, MessageHandler>;
10
+ }
11
+
12
+ /**
13
+ * In-process message bus connecting one or more {@link InMemoryTransport}
14
+ * instances. Routes broadcasts to every registered actor and directed messages
15
+ * to the actor that owns the recipient DID — with no relay, server, or network.
16
+ *
17
+ * Each delivery does a JSON round-trip (Uint8Array preserved as `__bytes` hex)
18
+ * so handlers receive an isolated, serialization-faithful copy, exactly as a
19
+ * real transport would. The message `body` is merged to the top level to match
20
+ * the shape the {@link NostrTransport} dispatch produces.
21
+ *
22
+ * @class InMemoryBus
23
+ */
24
+ export class InMemoryBus {
25
+ #transports: Set<InMemoryTransport> = new Set();
26
+
27
+ /** Attach a transport to this bus. Called by the transport's constructor. */
28
+ register(transport: InMemoryTransport): void {
29
+ this.#transports.add(transport);
30
+ }
31
+
32
+ /** Detach a transport from this bus. */
33
+ unregister(transport: InMemoryTransport): void {
34
+ this.#transports.delete(transport);
35
+ }
36
+
37
+ /**
38
+ * Deliver a message. With no `recipient` the message is broadcast to every
39
+ * actor on the bus; otherwise it is routed to the single transport that owns
40
+ * the recipient DID.
41
+ */
42
+ async deliver(message: BaseMessage, _sender: string, recipient?: string): Promise<void> {
43
+ const type = (message as { type?: string }).type;
44
+ if(!type) return;
45
+
46
+ // JSON round-trip to mimic transport serialization, preserving Uint8Array.
47
+ const replacer = (_k: string, v: unknown): unknown => v instanceof Uint8Array ? { __bytes: bytesToHex(v) } : v;
48
+ const reviver = (_k: string, v: unknown): unknown =>
49
+ v && typeof v === 'object' && '__bytes' in (v as Record<string, unknown>)
50
+ ? hexToBytes((v as { __bytes: string }).__bytes)
51
+ : v;
52
+ const raw = JSON.parse(JSON.stringify(message, replacer), reviver) as Record<string, unknown>;
53
+ const serialized = { ...raw, ...((raw.body as Record<string, unknown> | undefined) ?? {}) };
54
+
55
+ if(!recipient) {
56
+ for(const t of this.#transports) {
57
+ await t.dispatchBroadcast(type, serialized);
58
+ }
59
+ return;
60
+ }
61
+ for(const t of this.#transports) {
62
+ if(t.hasActor(recipient)) {
63
+ await t.dispatchDirected(recipient, type, serialized);
64
+ return;
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * In-process {@link Transport} that routes aggregation messages through an
72
+ * {@link InMemoryBus} instead of a relay or HTTP server. Supports multiple
73
+ * actors per instance, so a single transport can host both a service and its
74
+ * participants (e.g. a cohort-of-one via {@link AggregationRunner.solo}).
75
+ *
76
+ * Encryption is a no-op (in-process, same trust domain); `registerPeer` /
77
+ * `getPeerPk` keep a registry so the contract matches the wire transports.
78
+ *
79
+ * @class InMemoryTransport
80
+ * @implements {Transport}
81
+ */
82
+ export class InMemoryTransport implements Transport {
83
+ name: string = 'in-memory';
84
+ readonly bus: InMemoryBus;
85
+
86
+ #actors: Map<string, ActorEntry> = new Map();
87
+ #peers: Map<string, Uint8Array> = new Map();
88
+
89
+ /** @param bus Shared bus. Pass the same bus to connect multiple transports. */
90
+ constructor(bus: InMemoryBus = new InMemoryBus()) {
91
+ this.bus = bus;
92
+ this.bus.register(this);
93
+ }
94
+
95
+ start(): void {
96
+ // No-op: there is no underlying connection to open.
97
+ }
98
+
99
+ registerActor(did: string, keys: SchnorrKeyPair): void {
100
+ this.#actors.set(did, { keys, handlers: new Map() });
101
+ }
102
+
103
+ getActorPk(did: string): Uint8Array | undefined {
104
+ return this.#actors.get(did)?.keys.publicKey.compressed;
105
+ }
106
+
107
+ /** True if `did` is registered on this transport. Used by the bus for routing. */
108
+ hasActor(did: string): boolean {
109
+ return this.#actors.has(did);
110
+ }
111
+
112
+ registerPeer(did: string, communicationPk: Uint8Array): void {
113
+ this.#peers.set(did, communicationPk);
114
+ }
115
+
116
+ getPeerPk(did: string): Uint8Array | undefined {
117
+ return this.#peers.get(did);
118
+ }
119
+
120
+ registerMessageHandler(actorDid: string, messageType: string, handler: MessageHandler): void {
121
+ const actor = this.#actors.get(actorDid);
122
+ if(actor) actor.handlers.set(messageType, handler);
123
+ }
124
+
125
+ unregisterMessageHandler(actorDid: string, messageType: string): void {
126
+ const actor = this.#actors.get(actorDid);
127
+ if(actor) actor.handlers.delete(messageType);
128
+ }
129
+
130
+ unregisterActor(did: string): void {
131
+ const actor = this.#actors.get(did);
132
+ if(!actor) return;
133
+ actor.handlers.clear();
134
+ this.#actors.delete(did);
135
+ this.#peers.delete(did);
136
+ }
137
+
138
+ async sendMessage(message: BaseMessage, sender: string, recipient?: string): Promise<void> {
139
+ await this.bus.deliver(message, sender, recipient);
140
+ }
141
+
142
+ publishRepeating(
143
+ message: BaseMessage,
144
+ sender: string,
145
+ intervalMs: number,
146
+ recipient?: string,
147
+ ): () => void {
148
+ let stopped = false;
149
+ void this.sendMessage(message, sender, recipient).catch(() => { /* in-process: no relay to reject */ });
150
+ const timer = setInterval(() => {
151
+ if(stopped) return;
152
+ void this.sendMessage(message, sender, recipient).catch(() => { /* ignore */ });
153
+ }, intervalMs);
154
+ return () => {
155
+ if(stopped) return;
156
+ stopped = true;
157
+ clearInterval(timer);
158
+ };
159
+ }
160
+
161
+ /** Deliver a broadcast message to every actor on this transport that handles `type`. */
162
+ async dispatchBroadcast(type: string, message: unknown): Promise<void> {
163
+ for(const actor of this.#actors.values()) {
164
+ const handler = actor.handlers.get(type);
165
+ if(handler) await handler(message);
166
+ }
167
+ }
168
+
169
+ /** Deliver a directed message to the recipient actor's handler for `type`. */
170
+ async dispatchDirected(recipientDid: string, type: string, message: unknown): Promise<void> {
171
+ const handler = this.#actors.get(recipientDid)?.handlers.get(type);
172
+ if(handler) await handler(message);
173
+ }
174
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './transport.js';
2
2
  export * from './error.js';
3
3
  export * from './factory.js';
4
+ export * from './in-memory.js';
4
5
  export * from './nostr.js';
5
6
  export * from './didcomm.js';
6
7
  export * from './http/index.js';
@@ -89,7 +89,7 @@ export function deriveSingletonAddress(
89
89
  }
90
90
 
91
91
  /**
92
- * Options accepted by {@link Beacon.buildSignAndBroadcast} and related helpers.
92
+ * Options accepted by {@link SinglePartyBeacon.buildSignAndBroadcast} and related helpers.
93
93
  */
94
94
  export interface BroadcastOptions {
95
95
  /** Fee estimator for computing the transaction fee. Defaults to {@link DEFAULT_FEE_ESTIMATOR}. */
@@ -115,14 +115,14 @@ export interface BeaconTxPlan {
115
115
  feeSats: bigint;
116
116
  /**
117
117
  * Singleton beacon script kind, when applicable. Drives the signing dispatch
118
- * in {@link Beacon.signSinglePartyTx}. Aggregation plans set this to `'p2tr'`.
118
+ * in {@link SinglePartyBeacon.signSinglePartyTx}. Aggregation plans set this to `'p2tr'`.
119
119
  */
120
120
  scriptKind: SingletonScriptKind;
121
121
  }
122
122
 
123
123
  /**
124
124
  * Build an OP_RETURN script carrying a 32-byte beacon signal.
125
- * Exported as a utility so callers building txs outside Beacon (e.g., the aggregation
125
+ * Exported as a utility so callers building txs outside SinglePartyBeacon (e.g., the aggregation
126
126
  * `onProvideTxData` callback) can produce identical output.
127
127
  *
128
128
  * Uses the opcode *string* `'RETURN'` rather than the numeric `OP.RETURN`
@@ -168,7 +168,7 @@ async function fetchSpendableUtxo(
168
168
  * Returns the unsigned Transaction + prev-output metadata that an aggregation service's
169
169
  * signing session consumes (via {@link SigningTxData}).
170
170
  *
171
- * This is the reusable counterpart to {@link Beacon.buildSignAndBroadcast}'s internal
171
+ * This is the reusable counterpart to {@link SinglePartyBeacon.buildSignAndBroadcast}'s internal
172
172
  * construction step — the aggregation path must produce an unsigned tx because the
173
173
  * signature comes from a MuSig2 round, not a local secret key.
174
174
  *
@@ -311,9 +311,11 @@ async function signSingletonInput(
311
311
  }
312
312
 
313
313
  /**
314
- * Abstract base class for all BTCR2 Beacon types.
315
- * A Beacon is a service listed in a BTCR2 DID document that informs resolvers
316
- * how to find authentic updates to the DID.
314
+ * Abstract base class providing the single-party broadcast machinery shared by
315
+ * all BTCR2 beacon types: one party holds one key and broadcasts one 32-byte
316
+ * signal (P2PKH / P2WPKH / P2TR key-path). The aggregation (cohort of N >= 1)
317
+ * broadcast mode is the orthogonal axis, handled by the AggregationService and
318
+ * {@link buildAggregationBeaconTx}, not by this class hierarchy. See ADR 037.
317
319
  *
318
320
  * Beacons are lightweight typed wrappers around a {@link BeaconService} configuration.
319
321
  * Dependencies (signals, sidecar data, bitcoin connection) are passed as method
@@ -322,10 +324,10 @@ async function signSingletonInput(
322
324
  * Use {@link BeaconFactory.establish} to create typed instances from service config.
323
325
  *
324
326
  * @abstract
325
- * @class Beacon
326
- * @type {Beacon}
327
+ * @class SinglePartyBeacon
328
+ * @type {SinglePartyBeacon}
327
329
  */
328
- export abstract class Beacon {
330
+ export abstract class SinglePartyBeacon {
329
331
  /**
330
332
  * The Beacon service configuration parsed from the DID Document.
331
333
  */
@@ -5,7 +5,7 @@ import type { Signer } from '@did-btcr2/keypair';
5
5
  import type { BeaconProcessResult, DataNeed } from '../resolver.js';
6
6
  import type { SidecarData } from '../types.js';
7
7
  import type { BroadcastOptions } from './beacon.js';
8
- import { Beacon } from './beacon.js';
8
+ import { SinglePartyBeacon } from './beacon.js';
9
9
  import type { BeaconService, BeaconSignal, BlockMetadata, CasPublishFn } from './interfaces.js';
10
10
 
11
11
  /**
@@ -28,9 +28,9 @@ export interface CASBroadcastOptions extends BroadcastOptions {
28
28
  *
29
29
  * @class CASBeacon
30
30
  * @type {CASBeacon}
31
- * @extends {Beacon}
31
+ * @extends {SinglePartyBeacon}
32
32
  */
33
- export class CASBeacon extends Beacon {
33
+ export class CASBeacon extends SinglePartyBeacon {
34
34
  /**
35
35
  * Creates an instance of CASBeacon.
36
36
  * @param {BeaconService} service The service of the Beacon.
@@ -115,7 +115,7 @@ export class CASBeacon extends Beacon {
115
115
  * Creates a CAS Announcement mapping the DID to the update hash, broadcasts the hash of the
116
116
  * announcement via OP_RETURN, and optionally publishes the announcement off-chain via the
117
117
  * supplied `casPublish` callback. UTXO selection, PSBT construction, fee estimation, signing,
118
- * and broadcast are delegated to {@link Beacon.buildSignAndBroadcast}.
118
+ * and broadcast are delegated to {@link SinglePartyBeacon.buildSignAndBroadcast}.
119
119
  *
120
120
  * @param {SignedBTCR2Update} signedUpdate The signed BTCR2 update to broadcast.
121
121
  * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
@@ -1,5 +1,5 @@
1
1
  import { MethodError } from '@did-btcr2/common';
2
- import type { Beacon } from './beacon.js';
2
+ import type { SinglePartyBeacon } from './beacon.js';
3
3
  import { CASBeacon } from './cas-beacon.js';
4
4
  import type { BeaconService } from './interfaces.js';
5
5
  import { SingletonBeacon } from './singleton-beacon.js';
@@ -14,9 +14,9 @@ export class BeaconFactory {
14
14
  /**
15
15
  * Establish a Beacon instance based on the provided service and optional sidecar data.
16
16
  * @param {BeaconService} service The beacon service configuration.
17
- * @returns {Beacon} The established Beacon instance.
17
+ * @returns {SinglePartyBeacon} The established Beacon instance.
18
18
  */
19
- static establish(service: BeaconService): Beacon {
19
+ static establish(service: BeaconService): SinglePartyBeacon {
20
20
  switch (service.type) {
21
21
  case 'SingletonBeacon':
22
22
  return new SingletonBeacon(service);
@@ -5,16 +5,16 @@ import type { Signer } from '@did-btcr2/keypair';
5
5
  import type { BeaconProcessResult, DataNeed } from '../resolver.js';
6
6
  import type { SidecarData } from '../types.js';
7
7
  import type { BroadcastOptions } from './beacon.js';
8
- import { Beacon } from './beacon.js';
8
+ import { SinglePartyBeacon } from './beacon.js';
9
9
  import type { BeaconService, BeaconSignal, BlockMetadata } from './interfaces.js';
10
10
 
11
11
  /**
12
12
  * Implements {@link https://dcdpr.github.io/did-btcr2/terminology.html#singleton-beacon | Singleton Beacon}.
13
13
  * @class SingletonBeacon
14
14
  * @type {SingletonBeacon}
15
- * @extends {Beacon}
15
+ * @extends {SinglePartyBeacon}
16
16
  */
17
- export class SingletonBeacon extends Beacon {
17
+ export class SingletonBeacon extends SinglePartyBeacon {
18
18
 
19
19
  /**
20
20
  * Creates an instance of SingletonBeacon.
@@ -64,7 +64,7 @@ export class SingletonBeacon extends Beacon {
64
64
  *
65
65
  * The signal bytes embedded in OP_RETURN are the SHA-256 canonical hash of the signed update.
66
66
  * UTXO selection, PSBT construction, fee estimation, signing, and broadcast are delegated to
67
- * {@link Beacon.buildSignAndBroadcast}.
67
+ * {@link SinglePartyBeacon.buildSignAndBroadcast}.
68
68
  *
69
69
  * @param {SignedBTCR2Update} signedUpdate The signed BTCR2 update to broadcast.
70
70
  * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
@@ -2,12 +2,12 @@ import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
2
  import { canonicalize } from '@did-btcr2/common';
3
3
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
4
4
  import type { Signer } from '@did-btcr2/keypair';
5
- import { blockHash, BTCR2MerkleTree, didToIndex, hexToHash, verifySerializedProof } from '@did-btcr2/smt';
5
+ import { base64UrlToHash, blockHash, BTCR2MerkleTree, didToIndex, hashToHex, verifySerializedProof } from '@did-btcr2/smt';
6
6
  import { randomBytes } from '@noble/hashes/utils';
7
7
  import type { BeaconProcessResult, DataNeed } from '../resolver.js';
8
8
  import type { SidecarData } from '../types.js';
9
9
  import type { BroadcastOptions } from './beacon.js';
10
- import { Beacon } from './beacon.js';
10
+ import { SinglePartyBeacon } from './beacon.js';
11
11
  import { SMTBeaconError } from './error.js';
12
12
  import type { BeaconService, BeaconSignal, BlockMetadata } from './interfaces.js';
13
13
 
@@ -21,9 +21,9 @@ import type { BeaconService, BeaconSignal, BlockMetadata } from './interfaces.js
21
21
  *
22
22
  * @class SMTBeacon
23
23
  * @type {SMTBeacon}
24
- * @extends {Beacon}
24
+ * @extends {SinglePartyBeacon}
25
25
  */
26
- export class SMTBeacon extends Beacon {
26
+ export class SMTBeacon extends SinglePartyBeacon {
27
27
  /**
28
28
  * Creates an instance of SMTBeacon.
29
29
  * @param {BeaconService} service The Beacon service.
@@ -69,12 +69,7 @@ export class SMTBeacon extends Beacon {
69
69
  continue;
70
70
  }
71
71
 
72
- // Non-inclusion proof no update for this DID in this epoch, skip
73
- if(!smtProof.updateId) {
74
- continue;
75
- }
76
-
77
- // Nonce is required for proof verification
72
+ // Nonce is required for proof verification (inclusion and non-inclusion).
78
73
  if(!smtProof.nonce) {
79
74
  throw new SMTBeaconError(
80
75
  'SMT proof missing required nonce field.',
@@ -82,9 +77,15 @@ export class SMTBeacon extends Beacon {
82
77
  );
83
78
  }
84
79
 
85
- // Verify Merkle inclusion: leaf = hash(hash(nonce) || updateId)
80
+ // Verify the SMT proof against the on-chain root. Leaf value per spec:
81
+ // inclusion = hash(hash(nonce) || updateId); non-inclusion = hash(hash(nonce)).
82
+ // Hash fields are base64url (no padding) per the SMT Proof spec. A
83
+ // non-inclusion proof (absent updateId) is verified too, not trusted.
86
84
  const index = didToIndex(did);
87
- const candidateHash = blockHash(blockHash(hexToHash(smtProof.nonce)), hexToHash(smtProof.updateId));
85
+ const nonceHash = base64UrlToHash(smtProof.nonce);
86
+ const candidateHash = smtProof.updateId
87
+ ? blockHash(blockHash(nonceHash), base64UrlToHash(smtProof.updateId))
88
+ : blockHash(blockHash(nonceHash));
88
89
  const valid = verifySerializedProof(smtProof, index, candidateHash);
89
90
 
90
91
  if(!valid) {
@@ -94,14 +95,21 @@ export class SMTBeacon extends Beacon {
94
95
  );
95
96
  }
96
97
 
97
- // Look up the signed update in sidecar updateMap (keyed by hex canonical hash)
98
- const signedUpdate = sidecar.updateMap.get(smtProof.updateId);
98
+ // Non-inclusion proof verified no update for this DID this epoch, skip.
99
+ if(!smtProof.updateId) {
100
+ continue;
101
+ }
102
+
103
+ // Look up the signed update in sidecar updateMap (keyed by hex canonical
104
+ // hash). The proof's updateId is base64url, so convert to hex to match.
105
+ const updateHashHex = hashToHex(base64UrlToHash(smtProof.updateId));
106
+ const signedUpdate = sidecar.updateMap.get(updateHashHex);
99
107
 
100
108
  if(!signedUpdate) {
101
109
  // Signed update not available — emit a need
102
110
  needs.push({
103
111
  kind : 'NeedSignedUpdate',
104
- updateHash : smtProof.updateId,
112
+ updateHash : updateHashHex,
105
113
  beaconServiceId : this.service.id
106
114
  });
107
115
  continue;
@@ -119,7 +127,7 @@ export class SMTBeacon extends Beacon {
119
127
  * Builds a single-entry Sparse Merkle Tree from the signed update, then broadcasts the tree's
120
128
  * root hash via OP_RETURN. For multi-party aggregation, use the {@link AggregationService}
121
129
  * subsystem directly instead of this method. UTXO selection, PSBT construction, fee estimation,
122
- * signing, and broadcast are delegated to {@link Beacon.buildSignAndBroadcast}.
130
+ * signing, and broadcast are delegated to {@link SinglePartyBeacon.buildSignAndBroadcast}.
123
131
  *
124
132
  * @param {SignedBTCR2Update} signedUpdate The signed BTCR2 update to broadcast.
125
133
  * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
@@ -43,36 +43,39 @@ export interface ResolutionOptions extends DidResolutionOptions {
43
43
  * a path from a leaf in the tree to the Merkle root, proving that the leaf is in the tree.
44
44
  * See {@link https://dcdpr.github.io/did-btcr2/data-structures.html#smt-proof | SMT Proof (data structure)}.
45
45
  *
46
+ * All SHA-256 hash fields (`id`, `nonce`, `updateId`, `hashes`) are "base64url"
47
+ * [RFC4648] encoded without padding (43 chars each). `collapsed` is the 256-bit
48
+ * zero-node bitmap, also base64url no-pad (43 chars).
49
+ *
46
50
  * @example
47
51
  * ```json
48
52
  * {
49
- * "id": "<< Hexadecimal of Root Hash >>",
50
- * "nonce": "<< Hexadecimal of Nonce 1101 >>",
51
- * "updateId": "<< Hexadecimal of hash(Data Block 1101) >>",
52
- * "collapsed": "<< Hexadecimal of 0001 >>",
53
+ * "id": "q1H_iaYG0Oq6gbrycYL-r7FjUsJLnIpHDn49TLeONNA",
54
+ * "nonce": "99jndCBWHpZfmObXlIvRGHaPMgoQKXIETdD4H-XqryE",
55
+ * "updateId": "njYNViJq2OmhSw1fLfARPCj12RY3VXKGWdS3-7OQ2BE",
56
+ * "collapsed": "v_________________________________________8",
53
57
  * "hashes": [
54
- * "<< Hexadecimal of Hash 1110 >>",
55
- * "<< Hexadecimal of Hash 1001 >>",
56
- * "<< Hexadecimal of Hash 0 >>"
58
+ * "8JWXL7chPKJXwg-i9O1EFTHan_oOO_RmglDpu_ugax0"
57
59
  * ]
58
60
  * }
59
61
  * ```
60
62
  */
61
63
  export interface SMTProof {
62
64
  /**
63
- * The SHA-256 hash of the root node of the Sparse Merkle Tree.
65
+ * base64url (no padding) SHA-256 hash of the root node of the Sparse Merkle Tree.
64
66
  */
65
67
  id: string;
66
68
  /**
67
- * Optional 256-bit nonce generated for each update. Hex-encoded (64 chars).
69
+ * Optional 256-bit nonce generated for each update. base64url, no padding (43 chars).
68
70
  */
69
71
  nonce?: string;
70
72
  /**
71
- * Optional hex-encoded canonical hash of the BTCR2 Signed Update.
73
+ * Optional base64url (no padding) canonical hash of the BTCR2 Signed Update.
72
74
  */
73
75
  updateId?: string;
74
76
  /**
75
- * Bitmap of zero nodes within the path (see: collapsed leaves).
77
+ * base64url (no padding) bitmap of zero nodes within the path (see: collapsed
78
+ * leaves). Bit set = empty/zero sibling; bit clear = a sibling hash is present.
76
79
  */
77
80
  collapsed: string;
78
81
  /**
@@ -102,7 +102,7 @@ export type ResolverState =
102
102
  | { status: 'resolved'; result: DidResolutionResponse };
103
103
 
104
104
  /**
105
- * Return type from {@link Beacon.processSignals}.
105
+ * Return type from {@link SinglePartyBeacon.processSignals}.
106
106
  * Contains successfully resolved updates and any data needs that must be
107
107
  * satisfied before the remaining signals can be processed.
108
108
  */
@@ -281,11 +281,12 @@ export class Resolver {
281
281
  casMap.set(canonicalHash(update, { encoding: 'hex' }), update);
282
282
  }
283
283
 
284
- // SMT Proofs map
284
+ // SMT Proofs map. proof.id is base64url per the SMT Proof spec; key by the
285
+ // hex root hash so lookups match the hex signalBytes from the OP_RETURN.
285
286
  const smtMap = new Map<string, SMTProof>();
286
287
  if(sidecar.smtProofs?.length)
287
288
  for(const proof of sidecar.smtProofs) {
288
- smtMap.set(proof.id, proof);
289
+ smtMap.set(encodeHash(decodeHash(proof.id, 'base64urlnopad'), 'hex'), proof);
289
290
  }
290
291
 
291
292
  return { updateMap, casMap, smtMap };
@@ -723,10 +724,12 @@ export class Resolver {
723
724
  case 'NeedSMTProof': {
724
725
  const smtNeed = need as NeedSMTProof;
725
726
  const proof = data as SMTProof;
726
- if(proof.id !== smtNeed.smtRootHash) {
727
+ // proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
728
+ const proofIdHex = encodeHash(decodeHash(proof.id, 'base64urlnopad'), 'hex');
729
+ if(proofIdHex !== smtNeed.smtRootHash) {
727
730
  throw new ResolveError(
728
- `SMT proof root hash mismatch: expected ${smtNeed.smtRootHash}, got ${proof.id}`,
729
- INVALID_DID_UPDATE, { expected: smtNeed.smtRootHash, actual: proof.id }
731
+ `SMT proof root hash mismatch: expected ${smtNeed.smtRootHash}, got ${proofIdHex}`,
732
+ INVALID_DID_UPDATE, { expected: smtNeed.smtRootHash, actual: proofIdHex }
730
733
  );
731
734
  }
732
735
  this.#sidecarData.smtMap.set(smtNeed.smtRootHash, proof);