@fluxpointstudios/orynq-sdk-process-trace 0.2.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 +21 -0
- package/dist/index.cjs +1339 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1758 -0
- package/dist/index.d.ts +1758 -0
- package/dist/index.js +1285 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/.gitkeep +0 -0
- package/src/__tests__/bundle.test.ts +860 -0
- package/src/__tests__/integration.test.ts +611 -0
- package/src/__tests__/merkle.test.ts +756 -0
- package/src/__tests__/rolling-hash.test.ts +622 -0
- package/src/__tests__/trace-builder.test.ts +1012 -0
- package/src/__tests__/types.test.ts +414 -0
- package/src/bundle.ts +810 -0
- package/src/disclosure.ts +527 -0
- package/src/index.ts +265 -0
- package/src/manifest.ts +687 -0
- package/src/merkle.ts +428 -0
- package/src/rolling-hash.ts +366 -0
- package/src/trace-builder.ts +725 -0
- package/src/types.ts +522 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rolling hash computation for trace events using domain-separated SHA-256.
|
|
3
|
+
*
|
|
4
|
+
* Location: packages/process-trace/src/rolling-hash.ts
|
|
5
|
+
*
|
|
6
|
+
* This module implements cryptographic rolling hash computation for the process-trace
|
|
7
|
+
* package. Rolling hashes provide tamper-evident sequencing of trace events, ensuring
|
|
8
|
+
* that any modification to the event sequence is detectable.
|
|
9
|
+
*
|
|
10
|
+
* Domain Separation:
|
|
11
|
+
* - Event hashes use prefix "poi-trace:event:v1|" to prevent cross-context collisions
|
|
12
|
+
* - Rolling hashes use prefix "poi-trace:roll:v1|" for chain linking
|
|
13
|
+
* - Root hashes use prefix "poi-trace:root:v1|" for final commitment
|
|
14
|
+
*
|
|
15
|
+
* The rolling hash forms a hash chain: each hash incorporates the previous hash,
|
|
16
|
+
* creating an ordered, tamper-evident sequence. This is similar to blockchain
|
|
17
|
+
* block linking but at the event level.
|
|
18
|
+
*
|
|
19
|
+
* Used by:
|
|
20
|
+
* - TraceBuilder: Incrementally updates rolling hash as events are added
|
|
21
|
+
* - TraceBundle: Computes final root hash for the complete trace
|
|
22
|
+
* - TraceVerifier: Validates that event sequences have not been tampered with
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // Initialize rolling hash state
|
|
27
|
+
* const state = await initRollingHash();
|
|
28
|
+
*
|
|
29
|
+
* // Add events incrementally
|
|
30
|
+
* for (const event of events) {
|
|
31
|
+
* const eventHash = await computeEventHash(event);
|
|
32
|
+
* state = await updateRollingHash(state, eventHash);
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* // Or compute in batch
|
|
36
|
+
* const finalHash = await computeRollingHash(events);
|
|
37
|
+
*
|
|
38
|
+
* // Compute root hash including span hashes
|
|
39
|
+
* const rootHash = await computeRootHash(finalHash, spans);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
sha256StringHex,
|
|
45
|
+
canonicalize,
|
|
46
|
+
} from "@fluxpointstudios/orynq-sdk-core/utils";
|
|
47
|
+
|
|
48
|
+
import type { RollingHashState, TraceEvent, TraceSpan } from "./types.js";
|
|
49
|
+
import { HASH_DOMAIN_PREFIXES } from "./types.js";
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------------------------
|
|
52
|
+
// Constants
|
|
53
|
+
// -----------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Genesis seed for the initial rolling hash state.
|
|
57
|
+
* The first rolling hash is H("poi-trace:roll:v1|genesis").
|
|
58
|
+
*/
|
|
59
|
+
const GENESIS_SEED = "genesis";
|
|
60
|
+
|
|
61
|
+
// -----------------------------------------------------------------------------
|
|
62
|
+
// Event Hash Functions
|
|
63
|
+
// -----------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute hash for a single event using domain separation.
|
|
67
|
+
*
|
|
68
|
+
* The event hash is computed as:
|
|
69
|
+
* `H("poi-trace:event:v1|" + canonicalize(eventWithoutHash))`
|
|
70
|
+
*
|
|
71
|
+
* The 'hash' field is removed before hashing to avoid circularity - otherwise
|
|
72
|
+
* computing the hash would require knowing the hash.
|
|
73
|
+
*
|
|
74
|
+
* @param event - The trace event to hash
|
|
75
|
+
* @returns Promise resolving to the event hash as a lowercase hex string
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const event: TraceEvent = {
|
|
80
|
+
* kind: "command",
|
|
81
|
+
* id: "550e8400-e29b-41d4-a716-446655440000",
|
|
82
|
+
* seq: 1,
|
|
83
|
+
* timestamp: "2024-01-15T10:30:00.000Z",
|
|
84
|
+
* visibility: "public",
|
|
85
|
+
* command: "npm install",
|
|
86
|
+
* };
|
|
87
|
+
* const hash = await computeEventHash(event);
|
|
88
|
+
* // Returns 64-character hex string
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export async function computeEventHash(event: TraceEvent): Promise<string> {
|
|
92
|
+
// Create a copy without the hash field to avoid circularity
|
|
93
|
+
const eventWithoutHash = removeHashField(event);
|
|
94
|
+
|
|
95
|
+
// Canonicalize for deterministic serialization
|
|
96
|
+
const canonical = canonicalize(eventWithoutHash);
|
|
97
|
+
|
|
98
|
+
// Apply domain separation and hash
|
|
99
|
+
const prefixedData = HASH_DOMAIN_PREFIXES.event + canonical;
|
|
100
|
+
|
|
101
|
+
return sha256StringHex(prefixedData);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove the 'hash' field from an event object.
|
|
106
|
+
* Returns a shallow copy with all fields except 'hash'.
|
|
107
|
+
*
|
|
108
|
+
* @param event - Event to process
|
|
109
|
+
* @returns Event copy without the hash field
|
|
110
|
+
*/
|
|
111
|
+
function removeHashField<T extends { hash?: string }>(event: T): Omit<T, "hash"> {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
113
|
+
const { hash: _, ...rest } = event;
|
|
114
|
+
return rest;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// -----------------------------------------------------------------------------
|
|
118
|
+
// Rolling Hash State Management
|
|
119
|
+
// -----------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initialize rolling hash state with the genesis hash.
|
|
123
|
+
*
|
|
124
|
+
* The genesis hash is computed as:
|
|
125
|
+
* `H("poi-trace:roll:v1|genesis")`
|
|
126
|
+
*
|
|
127
|
+
* This provides a well-known starting point for all rolling hash chains,
|
|
128
|
+
* ensuring that empty traces have a deterministic hash value.
|
|
129
|
+
*
|
|
130
|
+
* @returns Promise resolving to the initial rolling hash state
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const state = await initRollingHash();
|
|
135
|
+
* console.log(state.currentHash); // Genesis hash
|
|
136
|
+
* console.log(state.itemCount); // 0
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export async function initRollingHash(): Promise<RollingHashState> {
|
|
140
|
+
const genesisInput = HASH_DOMAIN_PREFIXES.roll + GENESIS_SEED;
|
|
141
|
+
const genesisHash = await sha256StringHex(genesisInput);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
currentHash: genesisHash,
|
|
145
|
+
itemCount: 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update rolling hash state with a new event hash.
|
|
151
|
+
*
|
|
152
|
+
* The new rolling hash is computed as:
|
|
153
|
+
* `H("poi-trace:roll:v1|" + prevHash + "|" + eventHash)`
|
|
154
|
+
*
|
|
155
|
+
* This creates a hash chain where each hash depends on all previous hashes,
|
|
156
|
+
* making it impossible to modify earlier events without invalidating all
|
|
157
|
+
* subsequent hashes.
|
|
158
|
+
*
|
|
159
|
+
* @param state - Current rolling hash state
|
|
160
|
+
* @param eventHash - Hash of the event to add (from computeEventHash)
|
|
161
|
+
* @returns Promise resolving to the updated rolling hash state
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* let state = await initRollingHash();
|
|
166
|
+
*
|
|
167
|
+
* const eventHash = await computeEventHash(event);
|
|
168
|
+
* state = await updateRollingHash(state, eventHash);
|
|
169
|
+
*
|
|
170
|
+
* console.log(state.currentHash); // New rolling hash
|
|
171
|
+
* console.log(state.itemCount); // 1
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export async function updateRollingHash(
|
|
175
|
+
state: RollingHashState,
|
|
176
|
+
eventHash: string
|
|
177
|
+
): Promise<RollingHashState> {
|
|
178
|
+
// Construct the input: prefix + prevHash + "|" + eventHash
|
|
179
|
+
const input = HASH_DOMAIN_PREFIXES.roll + state.currentHash + "|" + eventHash;
|
|
180
|
+
const newHash = await sha256StringHex(input);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
currentHash: newHash,
|
|
184
|
+
itemCount: state.itemCount + 1,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// -----------------------------------------------------------------------------
|
|
189
|
+
// Batch Rolling Hash Computation
|
|
190
|
+
// -----------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Compute rolling hash for a sequence of events (batch mode).
|
|
194
|
+
*
|
|
195
|
+
* This function processes all events and returns the final rolling hash.
|
|
196
|
+
* Events are sorted by their `seq` field before processing to ensure
|
|
197
|
+
* deterministic ordering.
|
|
198
|
+
*
|
|
199
|
+
* The result is identical to calling `updateRollingHash` sequentially
|
|
200
|
+
* for each event, making it suitable for verification.
|
|
201
|
+
*
|
|
202
|
+
* @param events - Array of trace events to hash
|
|
203
|
+
* @returns Promise resolving to the final rolling hash as a hex string
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const events: TraceEvent[] = [
|
|
208
|
+
* { kind: "command", seq: 1, ... },
|
|
209
|
+
* { kind: "output", seq: 2, ... },
|
|
210
|
+
* { kind: "decision", seq: 3, ... },
|
|
211
|
+
* ];
|
|
212
|
+
*
|
|
213
|
+
* const finalHash = await computeRollingHash(events);
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export async function computeRollingHash(events: TraceEvent[]): Promise<string> {
|
|
217
|
+
// Sort events by seq to ensure deterministic ordering
|
|
218
|
+
const sortedEvents = [...events].sort((a, b) => a.seq - b.seq);
|
|
219
|
+
|
|
220
|
+
// Initialize with genesis hash
|
|
221
|
+
let state = await initRollingHash();
|
|
222
|
+
|
|
223
|
+
// Process each event in sequence
|
|
224
|
+
for (const event of sortedEvents) {
|
|
225
|
+
const eventHash = await computeEventHash(event);
|
|
226
|
+
state = await updateRollingHash(state, eventHash);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return state.currentHash;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -----------------------------------------------------------------------------
|
|
233
|
+
// Verification Functions
|
|
234
|
+
// -----------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Verify that a rolling hash matches the expected value for given events.
|
|
238
|
+
*
|
|
239
|
+
* This function recomputes the rolling hash from the events and compares
|
|
240
|
+
* it to the expected value. Used to verify trace integrity.
|
|
241
|
+
*
|
|
242
|
+
* @param events - Array of trace events to verify
|
|
243
|
+
* @param expectedHash - The expected rolling hash value
|
|
244
|
+
* @returns Promise resolving to true if the hash matches, false otherwise
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* const isValid = await verifyRollingHash(events, storedRollingHash);
|
|
249
|
+
* if (!isValid) {
|
|
250
|
+
* console.error("Trace has been tampered with!");
|
|
251
|
+
* }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export async function verifyRollingHash(
|
|
255
|
+
events: TraceEvent[],
|
|
256
|
+
expectedHash: string
|
|
257
|
+
): Promise<boolean> {
|
|
258
|
+
const computedHash = await computeRollingHash(events);
|
|
259
|
+
return constantTimeCompare(computedHash, expectedHash.toLowerCase());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Constant-time string comparison to prevent timing attacks.
|
|
264
|
+
*
|
|
265
|
+
* @param a - First string
|
|
266
|
+
* @param b - Second string
|
|
267
|
+
* @returns true if strings are equal
|
|
268
|
+
*/
|
|
269
|
+
function constantTimeCompare(a: string, b: string): boolean {
|
|
270
|
+
if (a.length !== b.length) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let result = 0;
|
|
275
|
+
for (let i = 0; i < a.length; i++) {
|
|
276
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result === 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// -----------------------------------------------------------------------------
|
|
283
|
+
// Root Hash Computation
|
|
284
|
+
// -----------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compute the final root hash from rolling hash and span hashes.
|
|
288
|
+
*
|
|
289
|
+
* The root hash is computed as:
|
|
290
|
+
* `H("poi-trace:root:v1|" + rollingHash + "|" + spanHash1 + "|" + spanHash2 + ...)`
|
|
291
|
+
*
|
|
292
|
+
* Spans are sorted by their `spanSeq` field before joining to ensure
|
|
293
|
+
* deterministic ordering. This creates a single commitment that covers
|
|
294
|
+
* both the event sequence (via rolling hash) and the span structure.
|
|
295
|
+
*
|
|
296
|
+
* @param rollingHash - The final rolling hash from all events
|
|
297
|
+
* @param spans - Array of trace spans (must have hash field populated)
|
|
298
|
+
* @returns Promise resolving to the root hash as a hex string
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const rollingHash = await computeRollingHash(events);
|
|
303
|
+
* const rootHash = await computeRootHash(rollingHash, spans);
|
|
304
|
+
*
|
|
305
|
+
* // rootHash can now be published as the trace commitment
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export async function computeRootHash(
|
|
309
|
+
rollingHash: string,
|
|
310
|
+
spans: TraceSpan[]
|
|
311
|
+
): Promise<string> {
|
|
312
|
+
// Sort spans by spanSeq for deterministic ordering
|
|
313
|
+
const sortedSpans = [...spans].sort((a, b) => a.spanSeq - b.spanSeq);
|
|
314
|
+
|
|
315
|
+
// Extract span hashes in order
|
|
316
|
+
const spanHashes = sortedSpans.map((span) => {
|
|
317
|
+
if (!span.hash) {
|
|
318
|
+
throw new Error(`Span ${span.id} is missing hash field`);
|
|
319
|
+
}
|
|
320
|
+
return span.hash;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Build the input string
|
|
324
|
+
// Format: prefix + rollingHash + "|" + spanHash1 + "|" + spanHash2 + ...
|
|
325
|
+
let input = HASH_DOMAIN_PREFIXES.root + rollingHash;
|
|
326
|
+
|
|
327
|
+
if (spanHashes.length > 0) {
|
|
328
|
+
input += "|" + spanHashes.join("|");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return sha256StringHex(input);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// -----------------------------------------------------------------------------
|
|
335
|
+
// Utility Exports for Testing
|
|
336
|
+
// -----------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Compute event hashes for multiple events in batch.
|
|
340
|
+
* Useful for pre-computing hashes before building a Merkle tree.
|
|
341
|
+
*
|
|
342
|
+
* @param events - Array of trace events
|
|
343
|
+
* @returns Promise resolving to array of event hashes in seq order
|
|
344
|
+
*/
|
|
345
|
+
export async function computeEventHashes(
|
|
346
|
+
events: TraceEvent[]
|
|
347
|
+
): Promise<string[]> {
|
|
348
|
+
// Sort by seq for deterministic ordering
|
|
349
|
+
const sortedEvents = [...events].sort((a, b) => a.seq - b.seq);
|
|
350
|
+
|
|
351
|
+
// Compute hashes in parallel for performance
|
|
352
|
+
const hashPromises = sortedEvents.map((event) => computeEventHash(event));
|
|
353
|
+
|
|
354
|
+
return Promise.all(hashPromises);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get the genesis hash for testing and verification.
|
|
359
|
+
* This is the initial hash value before any events are added.
|
|
360
|
+
*
|
|
361
|
+
* @returns Promise resolving to the genesis hash
|
|
362
|
+
*/
|
|
363
|
+
export async function getGenesisHash(): Promise<string> {
|
|
364
|
+
const state = await initRollingHash();
|
|
365
|
+
return state.currentHash;
|
|
366
|
+
}
|