/** * Deterministic challenge generation. * * Core function is pure: same inputs produce the same challenge. * Uses node:crypto SHA-256 for seed → PRNG → record/block selection. */ import { createHash, randomBytes } from "node:crypto"; import type { StorageChallenge, ChallengeConfig, ChallengeType } from "./types.js"; import { CHALLENGE_PROTOCOL_VERSION, DEFAULT_CHALLENGE_CONFIG } from "./types.js"; /** * Compute the current epoch from a timestamp and epoch duration. */ export function computeEpoch( timestampMs: number, epochDurationMs: number, ): number { return Math.floor(timestampMs / epochDurationMs); } /** * Generate a deterministic challenge ID from inputs. */ export function generateChallengeId( challengerDid: string, targetDid: string, subjectDid: string, epoch: number, nonce: string, ): string { const seed = `${challengerDid}:${targetDid}:${subjectDid}:${epoch}:${nonce}`; return createHash("sha256").update(seed).digest("hex").slice(0, 32); } /** * Create a deterministic PRNG from a seed string. * Uses SHA-256 hash chain for reproducible selection. */ function createPrng(seed: string): () => number { let state = createHash("sha256").update(seed).digest(); let offset = 0; return () => { if (offset + 4 > state.length) { state = createHash("sha256").update(state).digest(); offset = 0; } const value = state.readUInt32BE(offset) / 0xffffffff; offset += 4; return value; }; } /** * Select items deterministically from a pool using a seeded PRNG. */ function deterministicSample( items: T[], count: number, prng: () => number, ): T[] { if (items.length <= count) return [...items]; const selected: T[] = []; const pool = [...items]; for (let i = 0; i < count && pool.length > 0; i++) { const idx = Math.floor(prng() * pool.length); selected.push(pool[idx]!); pool.splice(idx, 1); } return selected; } /** * Generate a storage challenge. * * Pure function: same inputs produce the same challenge (deterministic). * The nonce is the only non-deterministic input — generate it externally * for auditability or pass a fixed value for testing. */ export function generateChallenge(params: { challengerDid: string; targetDid: string; subjectDid: string; commitCid: string; availableRecordPaths: string[]; availableBlockCids?: string[]; challengeType: ChallengeType; epoch: number; nonce?: string; config?: Partial; }): StorageChallenge { const config = { ...DEFAULT_CHALLENGE_CONFIG, ...params.config }; const nonce = params.nonce ?? randomBytes(16).toString("hex"); const id = generateChallengeId( params.challengerDid, params.targetDid, params.subjectDid, params.epoch, nonce, ); const prngSeed = `${id}:selection`; const prng = createPrng(prngSeed); // Select records for MST proof challenges let recordPaths: string[] = []; if ( params.challengeType === "mst-proof" || params.challengeType === "combined" ) { recordPaths = deterministicSample( params.availableRecordPaths, config.recordCount, prng, ); } // Select blocks for block-sample challenges let blockCids: string[] | undefined; if ( params.challengeType === "block-sample" || params.challengeType === "combined" ) { if (params.availableBlockCids && params.availableBlockCids.length > 0) { blockCids = deterministicSample( params.availableBlockCids, config.blockSampleSize, prng, ); } } const now = new Date(); const expiresAt = new Date(now.getTime() + config.expirationMs); return { id, version: CHALLENGE_PROTOCOL_VERSION, challengerDid: params.challengerDid, targetDid: params.targetDid, subjectDid: params.subjectDid, commitCid: params.commitCid, recordPaths, challengeType: params.challengeType, blockCids, epoch: params.epoch, nonce, issuedAt: now.toISOString(), expiresAt: expiresAt.toISOString(), }; }