atproto user agency toolkit for individuals and groups
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 156 lines 3.9 kB view raw
1/** 2 * Deterministic challenge generation. 3 * 4 * Core function is pure: same inputs produce the same challenge. 5 * Uses node:crypto SHA-256 for seed → PRNG → record/block selection. 6 */ 7 8import { createHash, randomBytes } from "node:crypto"; 9import type { StorageChallenge, ChallengeConfig, ChallengeType } from "./types.js"; 10import { CHALLENGE_PROTOCOL_VERSION, DEFAULT_CHALLENGE_CONFIG } from "./types.js"; 11 12/** 13 * Compute the current epoch from a timestamp and epoch duration. 14 */ 15export function computeEpoch( 16 timestampMs: number, 17 epochDurationMs: number, 18): number { 19 return Math.floor(timestampMs / epochDurationMs); 20} 21 22/** 23 * Generate a deterministic challenge ID from inputs. 24 */ 25export function generateChallengeId( 26 challengerDid: string, 27 targetDid: string, 28 subjectDid: string, 29 epoch: number, 30 nonce: string, 31): string { 32 const seed = `${challengerDid}:${targetDid}:${subjectDid}:${epoch}:${nonce}`; 33 return createHash("sha256").update(seed).digest("hex").slice(0, 32); 34} 35 36/** 37 * Create a deterministic PRNG from a seed string. 38 * Uses SHA-256 hash chain for reproducible selection. 39 */ 40function createPrng(seed: string): () => number { 41 let state = createHash("sha256").update(seed).digest(); 42 let offset = 0; 43 44 return () => { 45 if (offset + 4 > state.length) { 46 state = createHash("sha256").update(state).digest(); 47 offset = 0; 48 } 49 const value = state.readUInt32BE(offset) / 0xffffffff; 50 offset += 4; 51 return value; 52 }; 53} 54 55/** 56 * Select items deterministically from a pool using a seeded PRNG. 57 */ 58function deterministicSample<T>( 59 items: T[], 60 count: number, 61 prng: () => number, 62): T[] { 63 if (items.length <= count) return [...items]; 64 65 const selected: T[] = []; 66 const pool = [...items]; 67 68 for (let i = 0; i < count && pool.length > 0; i++) { 69 const idx = Math.floor(prng() * pool.length); 70 selected.push(pool[idx]!); 71 pool.splice(idx, 1); 72 } 73 74 return selected; 75} 76 77/** 78 * Generate a storage challenge. 79 * 80 * Pure function: same inputs produce the same challenge (deterministic). 81 * The nonce is the only non-deterministic input — generate it externally 82 * for auditability or pass a fixed value for testing. 83 */ 84export function generateChallenge(params: { 85 challengerDid: string; 86 targetDid: string; 87 subjectDid: string; 88 commitCid: string; 89 availableRecordPaths: string[]; 90 availableBlockCids?: string[]; 91 challengeType: ChallengeType; 92 epoch: number; 93 nonce?: string; 94 config?: Partial<ChallengeConfig>; 95}): StorageChallenge { 96 const config = { ...DEFAULT_CHALLENGE_CONFIG, ...params.config }; 97 const nonce = params.nonce ?? randomBytes(16).toString("hex"); 98 99 const id = generateChallengeId( 100 params.challengerDid, 101 params.targetDid, 102 params.subjectDid, 103 params.epoch, 104 nonce, 105 ); 106 107 const prngSeed = `${id}:selection`; 108 const prng = createPrng(prngSeed); 109 110 // Select records for MST proof challenges 111 let recordPaths: string[] = []; 112 if ( 113 params.challengeType === "mst-proof" || 114 params.challengeType === "combined" 115 ) { 116 recordPaths = deterministicSample( 117 params.availableRecordPaths, 118 config.recordCount, 119 prng, 120 ); 121 } 122 123 // Select blocks for block-sample challenges 124 let blockCids: string[] | undefined; 125 if ( 126 params.challengeType === "block-sample" || 127 params.challengeType === "combined" 128 ) { 129 if (params.availableBlockCids && params.availableBlockCids.length > 0) { 130 blockCids = deterministicSample( 131 params.availableBlockCids, 132 config.blockSampleSize, 133 prng, 134 ); 135 } 136 } 137 138 const now = new Date(); 139 const expiresAt = new Date(now.getTime() + config.expirationMs); 140 141 return { 142 id, 143 version: CHALLENGE_PROTOCOL_VERSION, 144 challengerDid: params.challengerDid, 145 targetDid: params.targetDid, 146 subjectDid: params.subjectDid, 147 commitCid: params.commitCid, 148 recordPaths, 149 challengeType: params.challengeType, 150 blockCids, 151 epoch: params.epoch, 152 nonce, 153 issuedAt: now.toISOString(), 154 expiresAt: expiresAt.toISOString(), 155 }; 156}