@waku/sds 0.0.8-d637425.0 → 0.0.8

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 (33) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bundle/index.js +614 -25
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/message_channel/events.d.ts +27 -3
  7. package/dist/message_channel/events.js +6 -0
  8. package/dist/message_channel/events.js.map +1 -1
  9. package/dist/message_channel/index.d.ts +1 -1
  10. package/dist/message_channel/message.d.ts +17 -13
  11. package/dist/message_channel/message.js +19 -11
  12. package/dist/message_channel/message.js.map +1 -1
  13. package/dist/message_channel/message_channel.d.ts +31 -3
  14. package/dist/message_channel/message_channel.js +99 -10
  15. package/dist/message_channel/message_channel.js.map +1 -1
  16. package/dist/message_channel/repair/buffers.d.ts +106 -0
  17. package/dist/message_channel/repair/buffers.js +206 -0
  18. package/dist/message_channel/repair/buffers.js.map +1 -0
  19. package/dist/message_channel/repair/repair.d.ts +92 -0
  20. package/dist/message_channel/repair/repair.js +209 -0
  21. package/dist/message_channel/repair/repair.js.map +1 -0
  22. package/dist/message_channel/repair/utils.d.ts +40 -0
  23. package/dist/message_channel/repair/utils.js +61 -0
  24. package/dist/message_channel/repair/utils.js.map +1 -0
  25. package/package.json +93 -1
  26. package/src/index.ts +7 -1
  27. package/src/message_channel/events.ts +28 -3
  28. package/src/message_channel/index.ts +1 -1
  29. package/src/message_channel/message.ts +24 -13
  30. package/src/message_channel/message_channel.ts +151 -17
  31. package/src/message_channel/repair/buffers.ts +277 -0
  32. package/src/message_channel/repair/repair.ts +331 -0
  33. package/src/message_channel/repair/utils.ts +80 -0
@@ -0,0 +1,277 @@
1
+ import { Logger } from "@waku/utils";
2
+
3
+ import type { HistoryEntry, MessageId } from "../message.js";
4
+
5
+ const log = new Logger("sds:repair:buffers");
6
+
7
+ /**
8
+ * Entry in the outgoing repair buffer with request timing
9
+ */
10
+ interface OutgoingBufferEntry {
11
+ entry: HistoryEntry;
12
+ tReq: number; // Timestamp when this repair request should be sent
13
+ requested: boolean; // Whether this repair has already been requested by the local node
14
+ }
15
+
16
+ /**
17
+ * Entry in the incoming repair buffer with response timing
18
+ */
19
+ interface IncomingBufferEntry {
20
+ entry: HistoryEntry;
21
+ tResp: number; // Timestamp when we should respond with this repair
22
+ }
23
+
24
+ /**
25
+ * Buffer for outgoing repair requests (messages we need)
26
+ * Maintains a sorted array by T_req for efficient retrieval of eligible entries
27
+ */
28
+ export class OutgoingRepairBuffer {
29
+ // Sorted array by T_req (ascending - earliest first)
30
+ private items: OutgoingBufferEntry[] = [];
31
+ private readonly maxSize: number;
32
+
33
+ public constructor(maxSize = 1000) {
34
+ this.maxSize = maxSize;
35
+ }
36
+
37
+ /**
38
+ * Add a missing message to the outgoing repair request buffer
39
+ * If message already exists, it is not updated (keeps original T_req)
40
+ * @returns true if the entry was added, false if it already existed
41
+ */
42
+ public add(entry: HistoryEntry, tReq: number): boolean {
43
+ const messageId = entry.messageId;
44
+
45
+ // Check if already exists - do NOT update T_req per spec
46
+ if (this.has(messageId)) {
47
+ log.info(
48
+ `Message ${messageId} already in outgoing buffer, keeping original T_req`
49
+ );
50
+ return false;
51
+ }
52
+
53
+ // Check buffer size limit
54
+ if (this.items.length >= this.maxSize) {
55
+ // Evict furthest T_req entry (last in sorted array) to preserve repairs that need to be sent the soonest
56
+ const evicted = this.items.pop()!;
57
+ log.warn(
58
+ `Buffer full, evicted furthest entry ${evicted.entry.messageId} with T_req ${evicted.tReq}`
59
+ );
60
+ }
61
+
62
+ // Add new entry and re-sort
63
+ const newEntry: OutgoingBufferEntry = { entry, tReq, requested: false };
64
+ const combined = [...this.items, newEntry];
65
+
66
+ // Sort by T_req (ascending)
67
+ combined.sort((a, b) => a.tReq - b.tReq);
68
+
69
+ this.items = combined;
70
+ log.info(`Added ${messageId} to outgoing buffer with T_req: ${tReq}`);
71
+ return true;
72
+ }
73
+
74
+ /**
75
+ * Remove a message from the buffer (e.g., when received)
76
+ */
77
+ public remove(messageId: MessageId): void {
78
+ this.items = this.items.filter(
79
+ (item) => item.entry.messageId !== messageId
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Get eligible repair requests (where T_req <= currentTime)
85
+ * Returns up to maxRequests entries from the front of the sorted array
86
+ * Marks returned entries as requested but keeps them in buffer until received
87
+ */
88
+ public getEligible(
89
+ currentTime: number = Date.now(),
90
+ maxRequests = 3
91
+ ): HistoryEntry[] {
92
+ const eligible: HistoryEntry[] = [];
93
+
94
+ // Iterate from front of sorted array (earliest T_req first)
95
+ for (const item of this.items) {
96
+ // Since array is sorted, once we hit an item with tReq > currentTime,
97
+ // all remaining items also have tReq > currentTime
98
+ if (item.tReq > currentTime) {
99
+ break;
100
+ }
101
+
102
+ // Only return items that haven't been requested yet
103
+ if (!item.requested && eligible.length < maxRequests) {
104
+ eligible.push(item.entry);
105
+ // Mark as requested so we don't request it again
106
+ item.requested = true;
107
+ log.info(
108
+ `Repair request for ${item.entry.messageId} is eligible and marked as requested`
109
+ );
110
+ }
111
+
112
+ // If we've found enough eligible items, exit early
113
+ if (eligible.length >= maxRequests) {
114
+ break;
115
+ }
116
+ }
117
+
118
+ return eligible;
119
+ }
120
+
121
+ /**
122
+ * Check if a message is in the buffer
123
+ */
124
+ public has(messageId: MessageId): boolean {
125
+ return this.items.some((item) => item.entry.messageId === messageId);
126
+ }
127
+
128
+ /**
129
+ * Get the current buffer size
130
+ */
131
+ public get size(): number {
132
+ return this.items.length;
133
+ }
134
+
135
+ /**
136
+ * Clear all entries
137
+ */
138
+ public clear(): void {
139
+ this.items = [];
140
+ }
141
+
142
+ /**
143
+ * Get all entries (for testing/debugging)
144
+ */
145
+ public getAll(): HistoryEntry[] {
146
+ return this.items.map((item) => item.entry);
147
+ }
148
+
149
+ /**
150
+ * Get items array directly (for testing)
151
+ */
152
+ public getItems(): OutgoingBufferEntry[] {
153
+ return [...this.items];
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Buffer for incoming repair requests (repairs we need to send)
159
+ * Maintains a sorted array by T_resp for efficient retrieval of ready entries
160
+ */
161
+ export class IncomingRepairBuffer {
162
+ // Sorted array by T_resp (ascending - earliest first)
163
+ private items: IncomingBufferEntry[] = [];
164
+ private readonly maxSize: number;
165
+
166
+ public constructor(maxSize = 1000) {
167
+ this.maxSize = maxSize;
168
+ }
169
+
170
+ /**
171
+ * Add a repair request that we can fulfill
172
+ * If message already exists, it is ignored (not updated)
173
+ * @returns true if the entry was added, false if it already existed
174
+ */
175
+ public add(entry: HistoryEntry, tResp: number): boolean {
176
+ const messageId = entry.messageId;
177
+
178
+ // Check if already exists - ignore per spec
179
+ if (this.has(messageId)) {
180
+ log.info(`Message ${messageId} already in incoming buffer, ignoring`);
181
+ return false;
182
+ }
183
+
184
+ // Check buffer size limit
185
+ if (this.items.length >= this.maxSize) {
186
+ // Evict furthest T_resp entry (last in sorted array)
187
+ const evicted = this.items.pop()!;
188
+ log.warn(
189
+ `Buffer full, evicted furthest entry ${evicted.entry.messageId} with T_resp ${evicted.tResp}`
190
+ );
191
+ }
192
+
193
+ // Add new entry and re-sort
194
+ const newEntry: IncomingBufferEntry = { entry, tResp };
195
+ const combined = [...this.items, newEntry];
196
+
197
+ // Sort by T_resp (ascending)
198
+ combined.sort((a, b) => a.tResp - b.tResp);
199
+
200
+ this.items = combined;
201
+ log.info(`Added ${messageId} to incoming buffer with T_resp: ${tResp}`);
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * Remove a message from the buffer
207
+ */
208
+ public remove(messageId: MessageId): void {
209
+ this.items = this.items.filter(
210
+ (item) => item.entry.messageId !== messageId
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Get repairs ready to be sent (where T_resp <= currentTime)
216
+ * Removes and returns ready entries
217
+ */
218
+ public getReady(currentTime: number): HistoryEntry[] {
219
+ // Find cutoff point - first item with tResp > currentTime
220
+ // Since array is sorted, all items before this are ready
221
+ let cutoff = 0;
222
+ for (let i = 0; i < this.items.length; i++) {
223
+ if (this.items[i].tResp > currentTime) {
224
+ cutoff = i;
225
+ break;
226
+ }
227
+ // If we reach the end, all items are ready
228
+ cutoff = i + 1;
229
+ }
230
+
231
+ // Extract ready items and log them
232
+ const ready = this.items.slice(0, cutoff).map((item) => {
233
+ log.info(`Repair for ${item.entry.messageId} is ready to be sent`);
234
+ return item.entry;
235
+ });
236
+
237
+ // Keep only items after cutoff
238
+ this.items = this.items.slice(cutoff);
239
+
240
+ return ready;
241
+ }
242
+
243
+ /**
244
+ * Check if a message is in the buffer
245
+ */
246
+ public has(messageId: MessageId): boolean {
247
+ return this.items.some((item) => item.entry.messageId === messageId);
248
+ }
249
+
250
+ /**
251
+ * Get the current buffer size
252
+ */
253
+ public get size(): number {
254
+ return this.items.length;
255
+ }
256
+
257
+ /**
258
+ * Clear all entries
259
+ */
260
+ public clear(): void {
261
+ this.items = [];
262
+ }
263
+
264
+ /**
265
+ * Get all entries (for testing/debugging)
266
+ */
267
+ public getAll(): HistoryEntry[] {
268
+ return this.items.map((item) => item.entry);
269
+ }
270
+
271
+ /**
272
+ * Get items array directly (for testing)
273
+ */
274
+ public getItems(): IncomingBufferEntry[] {
275
+ return [...this.items];
276
+ }
277
+ }
@@ -0,0 +1,331 @@
1
+ import { Logger } from "@waku/utils";
2
+
3
+ import type { HistoryEntry, MessageId } from "../message.js";
4
+ import { Message } from "../message.js";
5
+ import type { ILocalHistory } from "../message_channel.js";
6
+
7
+ import { IncomingRepairBuffer, OutgoingRepairBuffer } from "./buffers.js";
8
+ import {
9
+ bigintToNumber,
10
+ calculateXorDistance,
11
+ combinedHash,
12
+ hashString,
13
+ ParticipantId
14
+ } from "./utils.js";
15
+
16
+ const log = new Logger("sds:repair:manager");
17
+
18
+ /**
19
+ * Per SDS-R spec: One response group per 128 participants
20
+ */
21
+ const PARTICIPANTS_PER_RESPONSE_GROUP = 128;
22
+
23
+ /**
24
+ * Event emitter callback for repair events
25
+ */
26
+ export type RepairEventEmitter = (event: string, detail: unknown) => void;
27
+
28
+ /**
29
+ * Configuration for SDS-R repair protocol
30
+ */
31
+ export interface RepairConfig {
32
+ /** Minimum wait time before requesting repair (milliseconds) */
33
+ tMin?: number;
34
+ /** Maximum wait time for repair window (milliseconds) */
35
+ tMax?: number;
36
+ /** Number of response groups for load distribution */
37
+ numResponseGroups?: number;
38
+ /** Maximum buffer size for repair requests */
39
+ bufferSize?: number;
40
+ }
41
+
42
+ /**
43
+ * Default configuration values based on spec recommendations
44
+ */
45
+ export const DEFAULT_REPAIR_CONFIG: Required<RepairConfig> = {
46
+ tMin: 30000, // 30 seconds
47
+ tMax: 120000, // 120 seconds
48
+ numResponseGroups: 1, // Recommendation is 1 group per PARTICIPANTS_PER_RESPONSE_GROUP participants
49
+ bufferSize: 1000
50
+ };
51
+
52
+ /**
53
+ * Manager for SDS-R repair protocol
54
+ * Handles repair request/response timing and coordination
55
+ */
56
+ export class RepairManager {
57
+ private readonly participantId: ParticipantId;
58
+ private readonly config: Required<RepairConfig>;
59
+ private readonly outgoingBuffer: OutgoingRepairBuffer;
60
+ private readonly incomingBuffer: IncomingRepairBuffer;
61
+ private readonly eventEmitter?: RepairEventEmitter;
62
+
63
+ public constructor(
64
+ participantId: ParticipantId,
65
+ config: RepairConfig = {},
66
+ eventEmitter?: RepairEventEmitter
67
+ ) {
68
+ this.participantId = participantId;
69
+ this.config = { ...DEFAULT_REPAIR_CONFIG, ...config };
70
+ this.eventEmitter = eventEmitter;
71
+
72
+ this.outgoingBuffer = new OutgoingRepairBuffer(this.config.bufferSize);
73
+ this.incomingBuffer = new IncomingRepairBuffer(this.config.bufferSize);
74
+
75
+ log.info(`RepairManager initialized for participant ${participantId}`);
76
+ }
77
+
78
+ /**
79
+ * Calculate T_req - when to request repair for a missing message
80
+ * Per spec: T_req = current_time + hash(participant_id, message_id) % (T_max - T_min) + T_min
81
+ */
82
+ public calculateTReq(messageId: MessageId, currentTime = Date.now()): number {
83
+ const hash = combinedHash(this.participantId, messageId);
84
+ const range = BigInt(this.config.tMax - this.config.tMin);
85
+ const offset = bigintToNumber(hash % range) + this.config.tMin;
86
+ return currentTime + offset;
87
+ }
88
+
89
+ /**
90
+ * Calculate T_resp - when to respond with a repair
91
+ * Per spec: T_resp = current_time + (distance * hash(message_id)) % T_max
92
+ * where distance = participant_id XOR sender_id
93
+ */
94
+ public calculateTResp(
95
+ senderId: ParticipantId,
96
+ messageId: MessageId,
97
+ currentTime = Date.now()
98
+ ): number {
99
+ const distance = calculateXorDistance(this.participantId, senderId);
100
+ const messageHash = hashString(messageId);
101
+ const product = distance * messageHash;
102
+ const offset = bigintToNumber(product % BigInt(this.config.tMax));
103
+ return currentTime + offset;
104
+ }
105
+
106
+ /**
107
+ * Determine if this participant is in the response group for a message
108
+ * Per spec: (hash(participant_id, message_id) % num_response_groups) ==
109
+ * (hash(sender_id, message_id) % num_response_groups)
110
+ */
111
+ public isInResponseGroup(
112
+ senderId: ParticipantId,
113
+ messageId: MessageId
114
+ ): boolean {
115
+ if (!senderId) {
116
+ // Cannot determine response group without sender_id
117
+ return false;
118
+ }
119
+
120
+ const numGroups = BigInt(this.config.numResponseGroups);
121
+ if (numGroups <= BigInt(1)) {
122
+ // Single group, everyone is in it
123
+ return true;
124
+ }
125
+
126
+ const participantGroup =
127
+ combinedHash(this.participantId, messageId) % numGroups;
128
+ const senderGroup = combinedHash(senderId, messageId) % numGroups;
129
+
130
+ return participantGroup === senderGroup;
131
+ }
132
+
133
+ /**
134
+ * Handle missing dependencies by adding them to outgoing repair buffer
135
+ * Called when causal dependencies are detected as missing
136
+ */
137
+ public markDependenciesMissing(
138
+ missingEntries: HistoryEntry[],
139
+ currentTime = Date.now()
140
+ ): void {
141
+ for (const entry of missingEntries) {
142
+ // Calculate when to request this repair
143
+ const tReq = this.calculateTReq(entry.messageId, currentTime);
144
+
145
+ // Add to outgoing buffer - only log and emit event if actually added
146
+ const wasAdded = this.outgoingBuffer.add(entry, tReq);
147
+
148
+ if (wasAdded) {
149
+ log.info(
150
+ `Added missing dependency ${entry.messageId} to repair buffer with T_req=${tReq}`
151
+ );
152
+
153
+ // Emit event
154
+ this.eventEmitter?.("RepairRequestQueued", {
155
+ messageId: entry.messageId,
156
+ tReq
157
+ });
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Handle receipt of a message - remove from repair buffers
164
+ * Called when a message is successfully received
165
+ */
166
+ public markMessageReceived(messageId: MessageId): void {
167
+ // Remove from both buffers as we no longer need to request or respond
168
+ const wasInOutgoing = this.outgoingBuffer.has(messageId);
169
+ const wasInIncoming = this.incomingBuffer.has(messageId);
170
+
171
+ if (wasInOutgoing) {
172
+ this.outgoingBuffer.remove(messageId);
173
+ log.info(
174
+ `Removed ${messageId} from outgoing repair buffer after receipt`
175
+ );
176
+ }
177
+
178
+ if (wasInIncoming) {
179
+ this.incomingBuffer.remove(messageId);
180
+ log.info(
181
+ `Removed ${messageId} from incoming repair buffer after receipt`
182
+ );
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Get repair requests that are eligible to be sent
188
+ * Returns up to maxRequests entries where T_req <= currentTime
189
+ */
190
+ public getRepairRequests(
191
+ maxRequests = 3,
192
+ currentTime = Date.now()
193
+ ): HistoryEntry[] {
194
+ return this.outgoingBuffer.getEligible(currentTime, maxRequests);
195
+ }
196
+
197
+ /**
198
+ * Process incoming repair requests from other participants
199
+ * Adds to incoming buffer if we can fulfill and are in response group
200
+ */
201
+ public processIncomingRepairRequests(
202
+ requests: HistoryEntry[],
203
+ localHistory: ILocalHistory,
204
+ currentTime = Date.now()
205
+ ): void {
206
+ for (const request of requests) {
207
+ // Remove from our own outgoing buffer (someone else is requesting it)
208
+ this.outgoingBuffer.remove(request.messageId);
209
+
210
+ // Check if we have this message
211
+ const message = localHistory.find(
212
+ (m) => m.messageId === request.messageId
213
+ );
214
+ if (!message) {
215
+ log.info(
216
+ `Cannot fulfill repair for ${request.messageId} - not in local history`
217
+ );
218
+ continue;
219
+ }
220
+
221
+ // Check if we're in the response group
222
+ if (!request.senderId) {
223
+ log.warn(
224
+ `Cannot determine response group for ${request.messageId} - missing sender_id`
225
+ );
226
+ continue;
227
+ }
228
+
229
+ if (!this.isInResponseGroup(request.senderId, request.messageId)) {
230
+ log.info(`Not in response group for ${request.messageId}`);
231
+ continue;
232
+ }
233
+
234
+ // Calculate when to respond
235
+ const tResp = this.calculateTResp(
236
+ request.senderId,
237
+ request.messageId,
238
+ currentTime
239
+ );
240
+
241
+ // Add to incoming buffer - only log and emit event if actually added
242
+ const wasAdded = this.incomingBuffer.add(request, tResp);
243
+
244
+ if (wasAdded) {
245
+ log.info(
246
+ `Will respond to repair request for ${request.messageId} at T_resp=${tResp}`
247
+ );
248
+
249
+ // Emit event
250
+ this.eventEmitter?.("RepairResponseQueued", {
251
+ messageId: request.messageId,
252
+ tResp
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Sweep outgoing buffer for repairs that should be requested
260
+ * Returns entries where T_req <= currentTime
261
+ */
262
+ public sweepOutgoingBuffer(
263
+ maxRequests = 3,
264
+ currentTime = Date.now()
265
+ ): HistoryEntry[] {
266
+ return this.getRepairRequests(maxRequests, currentTime);
267
+ }
268
+
269
+ /**
270
+ * Sweep incoming buffer for repairs ready to be sent
271
+ * Returns messages that should be rebroadcast
272
+ */
273
+ public sweepIncomingBuffer(
274
+ localHistory: ILocalHistory,
275
+ currentTime = Date.now()
276
+ ): Message[] {
277
+ const ready = this.incomingBuffer.getReady(currentTime);
278
+ const messages: Message[] = [];
279
+
280
+ for (const entry of ready) {
281
+ const message = localHistory.find((m) => m.messageId === entry.messageId);
282
+ if (message) {
283
+ messages.push(message);
284
+ log.info(`Sending repair for ${entry.messageId}`);
285
+ } else {
286
+ log.warn(`Message ${entry.messageId} no longer in local history`);
287
+ }
288
+ }
289
+
290
+ return messages;
291
+ }
292
+
293
+ /**
294
+ * Clear all buffers
295
+ */
296
+ public clear(): void {
297
+ this.outgoingBuffer.clear();
298
+ this.incomingBuffer.clear();
299
+ }
300
+
301
+ /**
302
+ * Update number of response groups (e.g., when participants change)
303
+ */
304
+ public updateResponseGroups(numParticipants: number): void {
305
+ if (
306
+ numParticipants < 0 ||
307
+ !Number.isFinite(numParticipants) ||
308
+ !Number.isInteger(numParticipants)
309
+ ) {
310
+ throw new Error(
311
+ `Invalid numParticipants: ${numParticipants}. Must be a positive integer.`
312
+ );
313
+ }
314
+
315
+ if (numParticipants > Number.MAX_SAFE_INTEGER) {
316
+ log.warn(
317
+ `numParticipants ${numParticipants} exceeds MAX_SAFE_INTEGER, using MAX_SAFE_INTEGER`
318
+ );
319
+ numParticipants = Number.MAX_SAFE_INTEGER;
320
+ }
321
+
322
+ // Per spec: num_response_groups = max(1, num_participants / PARTICIPANTS_PER_RESPONSE_GROUP)
323
+ this.config.numResponseGroups = Math.max(
324
+ 1,
325
+ Math.floor(numParticipants / PARTICIPANTS_PER_RESPONSE_GROUP)
326
+ );
327
+ log.info(
328
+ `Updated response groups to ${this.config.numResponseGroups} for ${numParticipants} participants`
329
+ );
330
+ }
331
+ }
@@ -0,0 +1,80 @@
1
+ import { sha256 } from "@noble/hashes/sha2";
2
+ import { bytesToHex } from "@waku/utils/bytes";
3
+
4
+ import type { MessageId } from "../message.js";
5
+
6
+ /**
7
+ * ParticipantId can be a string or converted to a numeric representation for XOR operations
8
+ */
9
+ export type ParticipantId = string;
10
+
11
+ /**
12
+ * Compute SHA256 hash and convert to integer for modulo operations
13
+ * Uses first 8 bytes of hash for the integer conversion
14
+ */
15
+ export function hashToInteger(input: string): bigint {
16
+ const hashBytes = sha256(new TextEncoder().encode(input));
17
+ // Use first 8 bytes for a 64-bit integer
18
+ const view = new DataView(hashBytes.buffer, 0, 8);
19
+ return view.getBigUint64(0, false); // big-endian
20
+ }
21
+
22
+ /**
23
+ * Compute combined hash for (participantId, messageId) and convert to integer
24
+ * This is used for T_req calculations and response group membership
25
+ */
26
+ export function combinedHash(
27
+ participantId: ParticipantId,
28
+ messageId: MessageId
29
+ ): bigint {
30
+ const combined = `${participantId}${messageId}`;
31
+ return hashToInteger(combined);
32
+ }
33
+
34
+ /**
35
+ * Convert ParticipantId to numeric representation for XOR operations
36
+ * TODO: Not per spec, further review needed
37
+ * The spec assumes participant IDs support XOR natively, but we're using
38
+ * SHA256 hash to ensure consistent numeric representation for string IDs
39
+ */
40
+ export function participantIdToNumeric(participantId: ParticipantId): bigint {
41
+ return hashToInteger(participantId);
42
+ }
43
+
44
+ /**
45
+ * Calculate XOR distance between two participant IDs
46
+ * Used for T_resp calculations where distance affects response timing
47
+ */
48
+ export function calculateXorDistance(
49
+ participantId1: ParticipantId,
50
+ participantId2: ParticipantId
51
+ ): bigint {
52
+ const numeric1 = participantIdToNumeric(participantId1);
53
+ const numeric2 = participantIdToNumeric(participantId2);
54
+ return numeric1 ^ numeric2;
55
+ }
56
+
57
+ /**
58
+ * Helper to convert bigint to number for timing calculations
59
+ * Ensures the result fits in JavaScript's number range
60
+ */
61
+ export function bigintToNumber(value: bigint): number {
62
+ // For timing calculations, we modulo by MAX_SAFE_INTEGER to ensure it fits
63
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
64
+ return Number(value % maxSafe);
65
+ }
66
+
67
+ /**
68
+ * Calculate hash for a single string (used for message_id in T_resp)
69
+ */
70
+ export function hashString(input: string): bigint {
71
+ return hashToInteger(input);
72
+ }
73
+
74
+ /**
75
+ * Convert a hash result to hex string for debugging/logging
76
+ */
77
+ export function hashToHex(input: string): string {
78
+ const hashBytes = sha256(new TextEncoder().encode(input));
79
+ return bytesToHex(hashBytes);
80
+ }