12345678910111213141516import { ReedSolomonErasure } from "@subspace/reed-solomon-erasure.wasm";17import {18 CHUNK_SIZE,19 CodexError,20 MAX_DATA_SHARDS_PER_STRIPE,21 computeParityShards,22} from "../../shared/codex-types";23import { getReedSolomon } from "./rs-loader";242526272829303132333435let cachedWasmBytes: Uint8Array<ArrayBuffer> | null = null;3637async function freshReedSolomon(): Promise<ReedSolomonErasure> {38 if (cachedWasmBytes) {39 return ReedSolomonErasure.fromBytes(cachedWasmBytes);40 }41 42 43 44 45 await getReedSolomon();46 if (!cachedWasmBytes) {47 cachedWasmBytes = await loadWasmBytes();48 }49 return ReedSolomonErasure.fromBytes(cachedWasmBytes);50}515253545556575859async function loadWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {60 const inPopup =61 typeof chrome !== "undefined" &&62 typeof chrome.runtime !== "undefined" &&63 typeof chrome.runtime.getURL === "function";64 if (inPopup) {65 const url = chrome.runtime.getURL("codex/reed_solomon_erasure_bg.wasm");66 const response = await fetch(url);67 if (!response.ok) {68 throw new CodexError(69 "ERASURE_CODING_VERIFICATION_FAILED",70 `Failed to fetch WASM: ${response.status}`,71 );72 }73 return new Uint8Array(await response.arrayBuffer());74 }75 const fs = await import("node:fs");76 const path = await import("node:path");77 const wasmPath = path.resolve(78 "node_modules/@subspace/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",79 );80 return new Uint8Array(fs.readFileSync(wasmPath));81}82838485@digitaldefiance86878889function resultMessage(code: number): string {90 switch (code) {91 case ReedSolomonErasure.RESULT_OK: return "ok";92 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS: return "too few shards";93 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_SHARDS: return "too many shards";94 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_DATA_SHARDS: return "too few data shards";95 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_DATA_SHARDS: return "too many data shards";96 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_PARITY_SHARDS: return "too few parity shards";97 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_PARITY_SHARDS: return "too many parity shards";98 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_BUFFER_SHARDS: return "too few buffer shards";99 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_BUFFER_SHARDS: return "too many buffer shards";100 case ReedSolomonErasure.RESULT_ERROR_INCORRECT_SHARD_SIZE: return "incorrect shard size";101 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT: return "too few shards present";102 case ReedSolomonErasure.RESULT_ERROR_EMPTY_SHARD: return "empty shard";103 case ReedSolomonErasure.RESULT_ERROR_INVALID_SHARD_FLAGS: return "invalid shard flags";104 case ReedSolomonErasure.RESULT_ERROR_INVALID_INDEX: return "invalid index";105 default: return `unknown result code ${code}`;106 }107}108109export interface EncodeResult {110 dataChunks: Uint8Array[];111 parityChunks: Uint8Array[];112 totalChunks: number;113 dataShards: number;114 parityShards: number;115}116117export interface MultiStripeEncodeResult {118 stripes: EncodeResult[];119 120 totalDataChunks: number;121 122 totalParityChunks: number;123 124 totalChunks: number;125}126127128129130131132133export function planStripeShardCounts(totalDataShards: number): number[] {134 if (totalDataShards <= 0) return [];135 const numStripes = Math.max(136 1,137 Math.ceil(totalDataShards / MAX_DATA_SHARDS_PER_STRIPE),138 );139 const base = Math.floor(totalDataShards / numStripes);140 const remainder = totalDataShards % numStripes;141 const counts: number[] = [];142 for (let i = 0; i < numStripes; i++) {143 counts.push(base + (i < remainder ? 1 : 0));144 }145 return counts;146}147148149150151152153154155156157158export async function encodeWithErasure(data: Uint8Array): Promise<EncodeResult> {159 const rs = await freshReedSolomon();160161 const dataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));162 const parityShards = computeParityShards(dataShards);163 const totalShards = dataShards + parityShards;164 if (totalShards > 256) {165 throw new CodexError(166 "ERASURE_CODING_VERIFICATION_FAILED",167 `Single RS stripe cannot exceed 256 shards (got ${totalShards}; ${data.length} bytes / ${CHUNK_SIZE}-byte chunks). Use encodeIntoStripes for large payloads.`,168 );169 }170 const buffer = new Uint8Array(totalShards * CHUNK_SIZE);171 buffer.set(data, 0); 172173 let code: number;174 try {175 code = rs.encode(buffer, dataShards, parityShards);176 } catch (err) {177 178 179 180 throw new CodexError(181 "ERASURE_CODING_VERIFICATION_FAILED",182 `Reed-Solomon encode trapped (data=${dataShards}, parity=${parityShards}): ${183 err instanceof Error ? err.message : String(err)184 }`,185 );186 }187 if (code !== ReedSolomonErasure.RESULT_OK) {188 throw new CodexError(189 "ERASURE_CODING_VERIFICATION_FAILED",190 `Reed-Solomon encode failed: ${resultMessage(code)}`,191 );192 }193194 195 196 const dataChunks: Uint8Array[] = [];197 for (let i = 0; i < dataShards; i++) {198 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));199 }200 const parityChunks: Uint8Array[] = [];201 for (let i = 0; i < parityShards; i++) {202 const base = (dataShards + i) * CHUNK_SIZE;203 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));204 }205206 return {207 dataChunks,208 parityChunks,209 totalChunks: totalShards,210 dataShards,211 parityShards,212 };213}214215216217218219220221222223224225226227export async function decodeWithErasure(228 chunks: Map<number, Uint8Array>,229 dataShards: number,230 totalShards: number,231): Promise<Uint8Array> {232 const parityShards = totalShards - dataShards;233 if (parityShards < 0) {234 throw new CodexError(235 "ERASURE_CODING_VERIFICATION_FAILED",236 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,237 );238 }239240 if (chunks.size < dataShards) {241 throw new CodexError(242 "INSUFFICIENT_CHUNKS",243 `Need at least ${dataShards} chunks, have ${chunks.size}`,244 );245 }246247 248 const firstChunk = chunks.values().next().value;249 if (!firstChunk || firstChunk.length === 0) {250 throw new CodexError(251 "ERASURE_CODING_VERIFICATION_FAILED",252 "First chunk is empty; cannot determine shard size",253 );254 }255 const shardSize = firstChunk.length;256257 const rs = await freshReedSolomon();258 const buffer = new Uint8Array(totalShards * shardSize);259 const available: boolean[] = new Array(totalShards).fill(false);260261 for (const [index, chunk] of chunks) {262 if (index < 0 || index >= totalShards) {263 throw new CodexError(264 "ERASURE_CODING_VERIFICATION_FAILED",265 `Chunk index ${index} out of range [0, ${totalShards})`,266 );267 }268 if (chunk.length !== shardSize) {269 throw new CodexError(270 "ERASURE_CODING_VERIFICATION_FAILED",271 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,272 );273 }274 buffer.set(chunk, index * shardSize);275 available[index] = true;276 }277278 let code: number;279 try {280 code = rs.reconstruct(buffer, dataShards, parityShards, available);281 } catch (err) {282 throw new CodexError(283 "ERASURE_CODING_VERIFICATION_FAILED",284 `Reed-Solomon reconstruct trapped (data=${dataShards}, parity=${parityShards}, have=${chunks.size}): ${285 err instanceof Error ? err.message : String(err)286 }`,287 );288 }289 if (code !== ReedSolomonErasure.RESULT_OK) {290 291 292 293 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {294 throw new CodexError(295 "INSUFFICIENT_CHUNKS",296 `Need at least ${dataShards} valid chunks for reconstruction`,297 );298 }299 throw new CodexError(300 "ERASURE_CODING_VERIFICATION_FAILED",301 `Reed-Solomon reconstruct failed: ${resultMessage(code)}`,302 );303 }304305 return buffer.slice(0, dataShards * shardSize);306}307308309310311312313314315316317export async function encodeIntoStripes(318 data: Uint8Array,319): Promise<MultiStripeEncodeResult> {320 const totalDataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));321 const stripeShardCounts = planStripeShardCounts(totalDataShards);322323 const stripes: EncodeResult[] = [];324 let byteCursor = 0;325 for (const shardCount of stripeShardCounts) {326 const stripeByteCap = shardCount * CHUNK_SIZE;327 const stripeEnd = Math.min(byteCursor + stripeByteCap, data.length);328 const stripeBytes = data.subarray(byteCursor, stripeEnd);329 const result = await encodeWithErasure(stripeBytes);330 331 332 333 334 335 336 if (result.dataShards !== shardCount) {337 throw new CodexError(338 "ERASURE_CODING_VERIFICATION_FAILED",339 `Stripe plan mismatch: planned ${shardCount} data shards, encoder produced ${result.dataShards}`,340 );341 }342 stripes.push(result);343 byteCursor += stripeByteCap;344 }345346 const totalDataChunks = stripes.reduce((s, x) => s + x.dataShards, 0);347 const totalParityChunks = stripes.reduce((s, x) => s + x.parityShards, 0);348 const totalChunks = totalDataChunks + totalParityChunks;349 return { stripes, totalDataChunks, totalParityChunks, totalChunks };350}