@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,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Selective disclosure of trace spans with Merkle proofs.
|
|
3
|
+
*
|
|
4
|
+
* Location: packages/process-trace/src/disclosure.ts
|
|
5
|
+
*
|
|
6
|
+
* This module implements selective disclosure functionality for trace bundles,
|
|
7
|
+
* enabling privacy-preserving audits and compliance workflows. It allows verifiers
|
|
8
|
+
* to prove the existence of specific spans without revealing the entire trace.
|
|
9
|
+
*
|
|
10
|
+
* Key Concepts:
|
|
11
|
+
* - Selective Disclosure: Reveal only specific spans from a trace bundle
|
|
12
|
+
* - Membership Proofs: Prove a span exists without revealing its contents
|
|
13
|
+
* - Full Disclosure: Prove existence AND reveal span data with events
|
|
14
|
+
*
|
|
15
|
+
* Disclosure Modes:
|
|
16
|
+
* - "membership": Merkle proof only - proves span exists with specific hash
|
|
17
|
+
* without exposing the actual span data. Useful for compliance checks.
|
|
18
|
+
* - "full": Merkle proof + span data + event data - allows verifier to
|
|
19
|
+
* recompute hashes and fully verify the span contents.
|
|
20
|
+
*
|
|
21
|
+
* Use Cases:
|
|
22
|
+
* - Audit: "Show me span 3" (full mode) - auditor sees exactly what happened
|
|
23
|
+
* - Compliance: "Prove span exists" (membership) - no data exposure
|
|
24
|
+
* - Selective sharing: Disclose only public spans to external parties
|
|
25
|
+
*
|
|
26
|
+
* Used by:
|
|
27
|
+
* - Audit workflows: Selective disclosure of trace spans
|
|
28
|
+
* - Compliance verification: Prove span existence without data exposure
|
|
29
|
+
* - API endpoints: Create and verify disclosure requests
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Full disclosure of specific spans
|
|
34
|
+
* const result = await selectiveDisclose(bundle, ["span-1", "span-3"], "full");
|
|
35
|
+
* for (const disclosed of result.disclosedSpans) {
|
|
36
|
+
* console.log("Span:", disclosed.span?.name);
|
|
37
|
+
* console.log("Events:", disclosed.events?.length);
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Membership proof only (no data exposure)
|
|
41
|
+
* const membershipResult = await selectiveDisclose(bundle, ["span-2"], "membership");
|
|
42
|
+
*
|
|
43
|
+
* // Verify disclosure against anchor
|
|
44
|
+
* const verification = await verifyDisclosure(result, anchor.rootHash, anchor.merkleRoot);
|
|
45
|
+
* if (!verification.valid) {
|
|
46
|
+
* console.error("Verification failed:", verification.errors);
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import type {
|
|
52
|
+
TraceBundle,
|
|
53
|
+
TraceSpan,
|
|
54
|
+
TraceEvent,
|
|
55
|
+
DisclosureMode,
|
|
56
|
+
DisclosureResult,
|
|
57
|
+
MerkleProof,
|
|
58
|
+
} from "./types.js";
|
|
59
|
+
import {
|
|
60
|
+
generateMerkleProof,
|
|
61
|
+
verifyMerkleProof,
|
|
62
|
+
buildSpanMerkleTree,
|
|
63
|
+
computeSpanHash,
|
|
64
|
+
} from "./merkle.js";
|
|
65
|
+
import { HASH_DOMAIN_PREFIXES } from "./types.js";
|
|
66
|
+
import { sha256StringHex } from "@fluxpointstudios/orynq-sdk-core/utils";
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// DISCLOSURE REQUEST
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Disclosure request structure for API use.
|
|
74
|
+
*
|
|
75
|
+
* This interface defines the shape of a disclosure request that can be
|
|
76
|
+
* transmitted over network APIs. It contains all the information needed
|
|
77
|
+
* to identify the bundle and specify which spans to disclose.
|
|
78
|
+
*
|
|
79
|
+
* @property bundleRootHash - The root hash of the bundle for identification
|
|
80
|
+
* @property bundleMerkleRoot - The Merkle root for verification
|
|
81
|
+
* @property spanIds - Array of span IDs to disclose
|
|
82
|
+
* @property mode - Disclosure mode (membership or full)
|
|
83
|
+
*/
|
|
84
|
+
export interface DisclosureRequest {
|
|
85
|
+
bundleRootHash: string;
|
|
86
|
+
bundleMerkleRoot: string;
|
|
87
|
+
spanIds: string[];
|
|
88
|
+
mode: DisclosureMode;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// HELPER FUNCTIONS
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a span can be disclosed (exists in bundle).
|
|
97
|
+
*
|
|
98
|
+
* This function performs a simple existence check to determine if a span
|
|
99
|
+
* with the given ID exists in the bundle. It searches the privateRun.spans
|
|
100
|
+
* array for a matching span ID.
|
|
101
|
+
*
|
|
102
|
+
* @param bundle - The trace bundle to check
|
|
103
|
+
* @param spanId - The ID of the span to look for
|
|
104
|
+
* @returns true if the span exists in the bundle, false otherwise
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* if (canDisclose(bundle, "span-123")) {
|
|
109
|
+
* const result = await selectiveDisclose(bundle, ["span-123"], "full");
|
|
110
|
+
* } else {
|
|
111
|
+
* console.error("Span not found in bundle");
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function canDisclose(bundle: TraceBundle, spanId: string): boolean {
|
|
116
|
+
return bundle.privateRun.spans.some((span) => span.id === spanId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get span index by ID (needed for proof generation).
|
|
121
|
+
*
|
|
122
|
+
* Returns the index of a span in the sorted spans array (sorted by spanSeq).
|
|
123
|
+
* This index is used for Merkle proof generation, as the proof depends on
|
|
124
|
+
* the position of the span's leaf in the Merkle tree.
|
|
125
|
+
*
|
|
126
|
+
* @param bundle - The trace bundle containing the span
|
|
127
|
+
* @param spanId - The ID of the span to find
|
|
128
|
+
* @returns The 0-indexed position of the span in the sorted array
|
|
129
|
+
* @throws Error if the span is not found in the bundle
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const index = getSpanIndex(bundle, "span-456");
|
|
134
|
+
* console.log(`Span is at index ${index} in the Merkle tree`);
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function getSpanIndex(bundle: TraceBundle, spanId: string): number {
|
|
138
|
+
// Sort spans by spanSeq for consistent ordering (matches Merkle tree order)
|
|
139
|
+
const sortedSpans = [...bundle.privateRun.spans].sort(
|
|
140
|
+
(a, b) => a.spanSeq - b.spanSeq
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const index = sortedSpans.findIndex((span) => span.id === spanId);
|
|
144
|
+
|
|
145
|
+
if (index === -1) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Span with ID "${spanId}" not found in bundle. ` +
|
|
148
|
+
`Available span IDs: ${sortedSpans.map((s) => s.id).join(", ")}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return index;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get events belonging to a specific span from the bundle.
|
|
157
|
+
*
|
|
158
|
+
* @param bundle - The trace bundle
|
|
159
|
+
* @param span - The span to get events for
|
|
160
|
+
* @returns Array of events sorted by seq
|
|
161
|
+
*/
|
|
162
|
+
function getSpanEventsFromBundle(
|
|
163
|
+
bundle: TraceBundle,
|
|
164
|
+
span: TraceSpan
|
|
165
|
+
): TraceEvent[] {
|
|
166
|
+
// Create event lookup map
|
|
167
|
+
const eventMap = new Map<string, TraceEvent>();
|
|
168
|
+
for (const event of bundle.privateRun.events) {
|
|
169
|
+
eventMap.set(event.id, event);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get events for this span and sort by seq
|
|
173
|
+
return span.eventIds
|
|
174
|
+
.map((id) => eventMap.get(id))
|
|
175
|
+
.filter((e): e is TraceEvent => e !== undefined)
|
|
176
|
+
.sort((a, b) => a.seq - b.seq);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// =============================================================================
|
|
180
|
+
// DISCLOSURE REQUEST CREATION
|
|
181
|
+
// =============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a disclosure request (for API use).
|
|
185
|
+
*
|
|
186
|
+
* This function creates a structured disclosure request object that can be
|
|
187
|
+
* serialized and transmitted over network APIs. The request contains all
|
|
188
|
+
* information needed to identify the bundle and specify which spans to disclose.
|
|
189
|
+
*
|
|
190
|
+
* @param bundle - The trace bundle to create a request for
|
|
191
|
+
* @param spanIds - Array of span IDs to request disclosure for
|
|
192
|
+
* @param mode - The disclosure mode (membership or full)
|
|
193
|
+
* @returns A DisclosureRequest object ready for transmission
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* const request = createDisclosureRequest(
|
|
198
|
+
* bundle,
|
|
199
|
+
* ["span-1", "span-3"],
|
|
200
|
+
* "full"
|
|
201
|
+
* );
|
|
202
|
+
*
|
|
203
|
+
* // Send request to disclosure service
|
|
204
|
+
* const response = await fetch("/api/disclose", {
|
|
205
|
+
* method: "POST",
|
|
206
|
+
* body: JSON.stringify(request),
|
|
207
|
+
* });
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export function createDisclosureRequest(
|
|
211
|
+
bundle: TraceBundle,
|
|
212
|
+
spanIds: string[],
|
|
213
|
+
mode: DisclosureMode
|
|
214
|
+
): DisclosureRequest {
|
|
215
|
+
return {
|
|
216
|
+
bundleRootHash: bundle.rootHash,
|
|
217
|
+
bundleMerkleRoot: bundle.merkleRoot,
|
|
218
|
+
spanIds: [...spanIds], // Create a copy to prevent external mutation
|
|
219
|
+
mode,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// SELECTIVE DISCLOSURE
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Selectively disclose specific spans from a bundle.
|
|
229
|
+
*
|
|
230
|
+
* This function generates disclosure results for the specified spans. Depending
|
|
231
|
+
* on the disclosure mode, it includes either just Merkle proofs (membership mode)
|
|
232
|
+
* or Merkle proofs plus full span and event data (full mode).
|
|
233
|
+
*
|
|
234
|
+
* The function validates that all requested spans exist in the bundle before
|
|
235
|
+
* proceeding with disclosure generation.
|
|
236
|
+
*
|
|
237
|
+
* @param bundle - The trace bundle containing all data
|
|
238
|
+
* @param spanIds - IDs of spans to disclose
|
|
239
|
+
* @param mode - Disclosure mode:
|
|
240
|
+
* - "membership": Merkle proof only (proves span exists with hash)
|
|
241
|
+
* - "full": Merkle proof + span data + event data
|
|
242
|
+
* @returns Promise resolving to DisclosureResult with proofs and optionally data
|
|
243
|
+
* @throws Error if any requested spanId does not exist in the bundle
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* // Full disclosure - includes span data and events
|
|
248
|
+
* const fullResult = await selectiveDisclose(bundle, ["span-1"], "full");
|
|
249
|
+
* console.log(fullResult.disclosedSpans[0].span?.name);
|
|
250
|
+
* console.log(fullResult.disclosedSpans[0].events?.length);
|
|
251
|
+
*
|
|
252
|
+
* // Membership disclosure - proof only, no data
|
|
253
|
+
* const membershipResult = await selectiveDisclose(bundle, ["span-1"], "membership");
|
|
254
|
+
* // membershipResult.disclosedSpans[0].span is undefined
|
|
255
|
+
* // membershipResult.disclosedSpans[0].events is undefined
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export async function selectiveDisclose(
|
|
259
|
+
bundle: TraceBundle,
|
|
260
|
+
spanIds: string[],
|
|
261
|
+
mode: DisclosureMode
|
|
262
|
+
): Promise<DisclosureResult> {
|
|
263
|
+
// Validate all spanIds exist in bundle
|
|
264
|
+
const missingSpanIds: string[] = [];
|
|
265
|
+
for (const spanId of spanIds) {
|
|
266
|
+
if (!canDisclose(bundle, spanId)) {
|
|
267
|
+
missingSpanIds.push(spanId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (missingSpanIds.length > 0) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Cannot disclose spans that do not exist in bundle: ${missingSpanIds.join(", ")}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Build the Merkle tree for proof generation
|
|
278
|
+
const merkleTree = await buildSpanMerkleTree(
|
|
279
|
+
bundle.privateRun.spans,
|
|
280
|
+
bundle.privateRun.events
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Sort spans by spanSeq for consistent indexing
|
|
284
|
+
const sortedSpans = [...bundle.privateRun.spans].sort(
|
|
285
|
+
(a, b) => a.spanSeq - b.spanSeq
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Create a map for quick span lookup
|
|
289
|
+
const spanMap = new Map<string, TraceSpan>();
|
|
290
|
+
for (const span of sortedSpans) {
|
|
291
|
+
spanMap.set(span.id, span);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Generate disclosures for each requested span
|
|
295
|
+
const disclosedSpans: DisclosureResult["disclosedSpans"] = [];
|
|
296
|
+
|
|
297
|
+
for (const spanId of spanIds) {
|
|
298
|
+
// Get the span and its index
|
|
299
|
+
const span = spanMap.get(spanId);
|
|
300
|
+
if (!span) {
|
|
301
|
+
// This should not happen since we validated above, but handle defensively
|
|
302
|
+
throw new Error(`Span "${spanId}" not found after validation`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const spanIndex = getSpanIndex(bundle, spanId);
|
|
306
|
+
|
|
307
|
+
// Generate Merkle proof
|
|
308
|
+
const proof = generateMerkleProof(merkleTree, spanIndex);
|
|
309
|
+
|
|
310
|
+
// Build the disclosed span entry
|
|
311
|
+
if (mode === "full") {
|
|
312
|
+
// Full mode: include span data and events
|
|
313
|
+
const events = getSpanEventsFromBundle(bundle, span);
|
|
314
|
+
|
|
315
|
+
disclosedSpans.push({
|
|
316
|
+
spanId,
|
|
317
|
+
proof,
|
|
318
|
+
span: { ...span }, // Clone to prevent external mutation
|
|
319
|
+
events: events.map((e) => ({ ...e })), // Clone events
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
// Membership mode: proof only, no data
|
|
323
|
+
disclosedSpans.push({
|
|
324
|
+
spanId,
|
|
325
|
+
proof,
|
|
326
|
+
// span and events are undefined in membership mode
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
mode,
|
|
333
|
+
rootHash: bundle.rootHash,
|
|
334
|
+
merkleRoot: bundle.merkleRoot,
|
|
335
|
+
disclosedSpans,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// =============================================================================
|
|
340
|
+
// VERIFICATION FUNCTIONS
|
|
341
|
+
// =============================================================================
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Verify a disclosure result against expected hashes.
|
|
345
|
+
*
|
|
346
|
+
* This function performs comprehensive verification of a disclosure result:
|
|
347
|
+
* 1. Checks that rootHash matches the expected value from the anchor
|
|
348
|
+
* 2. Checks that merkleRoot matches the expected value from the anchor
|
|
349
|
+
* 3. For each disclosed span, verifies the Merkle proof
|
|
350
|
+
* 4. For full mode disclosures, verifies span hash recomputation
|
|
351
|
+
*
|
|
352
|
+
* @param disclosure - The disclosure result to verify
|
|
353
|
+
* @param expectedRootHash - Expected root hash from anchor/on-chain commitment
|
|
354
|
+
* @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
|
|
355
|
+
* @returns Promise resolving to verification result with validity status and errors
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* const disclosure = await selectiveDisclose(bundle, ["span-1"], "full");
|
|
360
|
+
* const verification = await verifyDisclosure(
|
|
361
|
+
* disclosure,
|
|
362
|
+
* anchor.rootHash,
|
|
363
|
+
* anchor.merkleRoot
|
|
364
|
+
* );
|
|
365
|
+
*
|
|
366
|
+
* if (verification.valid) {
|
|
367
|
+
* console.log("Disclosure verified successfully");
|
|
368
|
+
* } else {
|
|
369
|
+
* console.error("Verification failed:", verification.errors);
|
|
370
|
+
* }
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
export async function verifyDisclosure(
|
|
374
|
+
disclosure: DisclosureResult,
|
|
375
|
+
expectedRootHash: string,
|
|
376
|
+
expectedMerkleRoot: string
|
|
377
|
+
): Promise<{ valid: boolean; errors: string[] }> {
|
|
378
|
+
const errors: string[] = [];
|
|
379
|
+
|
|
380
|
+
// Check rootHash matches expected
|
|
381
|
+
if (disclosure.rootHash !== expectedRootHash) {
|
|
382
|
+
errors.push(
|
|
383
|
+
`Root hash mismatch: disclosure has "${disclosure.rootHash}", ` +
|
|
384
|
+
`expected "${expectedRootHash}"`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check merkleRoot matches expected
|
|
389
|
+
if (disclosure.merkleRoot !== expectedMerkleRoot) {
|
|
390
|
+
errors.push(
|
|
391
|
+
`Merkle root mismatch: disclosure has "${disclosure.merkleRoot}", ` +
|
|
392
|
+
`expected "${expectedMerkleRoot}"`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Verify each disclosed span
|
|
397
|
+
for (const disclosed of disclosure.disclosedSpans) {
|
|
398
|
+
// Verify the Merkle proof for this span
|
|
399
|
+
const proofValid = await verifyMerkleProof(disclosed.proof);
|
|
400
|
+
|
|
401
|
+
if (!proofValid) {
|
|
402
|
+
errors.push(
|
|
403
|
+
`Merkle proof verification failed for span "${disclosed.spanId}"`
|
|
404
|
+
);
|
|
405
|
+
continue; // Skip further checks for this span
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Verify proof rootHash matches expected
|
|
409
|
+
if (disclosed.proof.rootHash !== expectedMerkleRoot) {
|
|
410
|
+
errors.push(
|
|
411
|
+
`Proof root hash mismatch for span "${disclosed.spanId}": ` +
|
|
412
|
+
`proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// For full mode with data, verify span hash recomputation
|
|
417
|
+
if (disclosure.mode === "full" && disclosed.span && disclosed.events) {
|
|
418
|
+
const spanVerification = await verifySpanDisclosure(
|
|
419
|
+
{
|
|
420
|
+
spanId: disclosed.spanId,
|
|
421
|
+
proof: disclosed.proof,
|
|
422
|
+
span: disclosed.span,
|
|
423
|
+
events: disclosed.events,
|
|
424
|
+
},
|
|
425
|
+
expectedMerkleRoot
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (!spanVerification.valid) {
|
|
429
|
+
errors.push(...spanVerification.errors);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
valid: errors.length === 0,
|
|
436
|
+
errors,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Verify a single span's disclosure (with data).
|
|
442
|
+
*
|
|
443
|
+
* This function performs detailed verification of a disclosed span:
|
|
444
|
+
* 1. Recomputes the span hash from the provided span and events
|
|
445
|
+
* 2. Computes the expected leaf hash from the span hash
|
|
446
|
+
* 3. Verifies the leaf hash matches the proof's leafHash
|
|
447
|
+
* 4. Verifies the Merkle proof is valid
|
|
448
|
+
*
|
|
449
|
+
* This is used for "full" mode disclosures where span data is provided.
|
|
450
|
+
*
|
|
451
|
+
* @param disclosed - Object containing spanId, proof, span data, and events
|
|
452
|
+
* @param expectedMerkleRoot - Expected Merkle root from anchor/on-chain commitment
|
|
453
|
+
* @returns Promise resolving to verification result with validity status and errors
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* const result = await verifySpanDisclosure(
|
|
458
|
+
* {
|
|
459
|
+
* spanId: "span-123",
|
|
460
|
+
* proof: merkleProof,
|
|
461
|
+
* span: spanData,
|
|
462
|
+
* events: spanEvents,
|
|
463
|
+
* },
|
|
464
|
+
* expectedMerkleRoot
|
|
465
|
+
* );
|
|
466
|
+
*
|
|
467
|
+
* if (result.valid) {
|
|
468
|
+
* console.log("Span data is authentic and included in the trace");
|
|
469
|
+
* }
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
export async function verifySpanDisclosure(
|
|
473
|
+
disclosed: {
|
|
474
|
+
spanId: string;
|
|
475
|
+
proof: MerkleProof;
|
|
476
|
+
span: TraceSpan;
|
|
477
|
+
events: TraceEvent[];
|
|
478
|
+
},
|
|
479
|
+
expectedMerkleRoot: string
|
|
480
|
+
): Promise<{ valid: boolean; errors: string[] }> {
|
|
481
|
+
const errors: string[] = [];
|
|
482
|
+
|
|
483
|
+
// Sort events by seq for deterministic hash computation
|
|
484
|
+
const sortedEvents = [...disclosed.events].sort((a, b) => a.seq - b.seq);
|
|
485
|
+
|
|
486
|
+
// Get event hashes in seq order
|
|
487
|
+
const eventHashes = sortedEvents.map((e) => e.hash ?? "");
|
|
488
|
+
|
|
489
|
+
// Recompute span hash from span + events
|
|
490
|
+
const computedSpanHash = await computeSpanHash(disclosed.span, eventHashes);
|
|
491
|
+
|
|
492
|
+
// Compute expected leaf hash: H("poi-trace:leaf:v1|" + spanHash)
|
|
493
|
+
const computedLeafHash = await sha256StringHex(
|
|
494
|
+
HASH_DOMAIN_PREFIXES.leaf + computedSpanHash
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Verify leaf hash matches proof.leafHash
|
|
498
|
+
if (computedLeafHash !== disclosed.proof.leafHash) {
|
|
499
|
+
errors.push(
|
|
500
|
+
`Span hash verification failed for "${disclosed.spanId}": ` +
|
|
501
|
+
`computed leaf hash "${computedLeafHash}" does not match ` +
|
|
502
|
+
`proof leaf hash "${disclosed.proof.leafHash}". ` +
|
|
503
|
+
`The span data may have been modified.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Verify Merkle proof
|
|
508
|
+
const proofValid = await verifyMerkleProof(disclosed.proof);
|
|
509
|
+
if (!proofValid) {
|
|
510
|
+
errors.push(
|
|
511
|
+
`Merkle proof verification failed for span "${disclosed.spanId}"`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Verify proof rootHash matches expected
|
|
516
|
+
if (disclosed.proof.rootHash !== expectedMerkleRoot) {
|
|
517
|
+
errors.push(
|
|
518
|
+
`Proof root hash mismatch for span "${disclosed.spanId}": ` +
|
|
519
|
+
`proof has "${disclosed.proof.rootHash}", expected "${expectedMerkleRoot}"`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
valid: errors.length === 0,
|
|
525
|
+
errors,
|
|
526
|
+
};
|
|
527
|
+
}
|