12345678910111213141516import { ReedSolomonErasure } from "@subspace/reed-solomon-erasure.wasm";17import {18 CHUNK_SIZE,19 CodexError,20 computeParityShards,21} from "../../shared/codex-types";22import { getReedSolomon } from "./rs-loader";232425262728293031323334let cachedWasmBytes: Uint8Array<ArrayBuffer> | null = null;3536async function freshReedSolomon(): Promise<ReedSolomonErasure> {37 if (cachedWasmBytes) {38 return ReedSolomonErasure.fromBytes(cachedWasmBytes);39 }40 41 42 43 44 await getReedSolomon();45 if (!cachedWasmBytes) {46 cachedWasmBytes = await loadWasmBytes();47 }48 return ReedSolomonErasure.fromBytes(cachedWasmBytes);49}505152535455565758async function loadWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {59 const inPopup =60 typeof chrome !== "undefined" &&61 typeof chrome.runtime !== "undefined" &&62 typeof chrome.runtime.getURL === "function";63 if (inPopup) {64 const url = chrome.runtime.getURL("codex/reed_solomon_erasure_bg.wasm");65 const response = await fetch(url);66 if (!response.ok) {67 throw new CodexError(68 "ERASURE_CODING_VERIFICATION_FAILED",69 `Failed to fetch WASM: ${response.status}`,70 );71 }72 return new Uint8Array(await response.arrayBuffer());73 }74 const fs = await import("node:fs");75 const path = await import("node:path");76 const wasmPath = path.resolve(77 "node_modules/@subspace/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",78 );79 return new Uint8Array(fs.readFileSync(wasmPath));80}81828384@digitaldefiance85868788function resultMessage(code: number): string {89 switch (code) {90 case ReedSolomonErasure.RESULT_OK: return "ok";91 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS: return "too few shards";92 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_SHARDS: return "too many shards";93 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_DATA_SHARDS: return "too few data shards";94 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_DATA_SHARDS: return "too many data shards";95 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_PARITY_SHARDS: return "too few parity shards";96 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_PARITY_SHARDS: return "too many parity shards";97 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_BUFFER_SHARDS: return "too few buffer shards";98 case ReedSolomonErasure.RESULT_ERROR_TOO_MANY_BUFFER_SHARDS: return "too many buffer shards";99 case ReedSolomonErasure.RESULT_ERROR_INCORRECT_SHARD_SIZE: return "incorrect shard size";100 case ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT: return "too few shards present";101 case ReedSolomonErasure.RESULT_ERROR_EMPTY_SHARD: return "empty shard";102 case ReedSolomonErasure.RESULT_ERROR_INVALID_SHARD_FLAGS: return "invalid shard flags";103 case ReedSolomonErasure.RESULT_ERROR_INVALID_INDEX: return "invalid index";104 default: return `unknown result code ${code}`;105 }106}107108export interface EncodeResult {109 dataChunks: Uint8Array[];110 parityChunks: Uint8Array[];111 totalChunks: number;112 dataShards: number;113 parityShards: number;114}115116117118119120121122export async function encodeWithErasure(data: Uint8Array): Promise<EncodeResult> {123 const rs = await freshReedSolomon();124125 const dataShards = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));126 const parityShards = computeParityShards(dataShards);127 const totalShards = dataShards + parityShards;128 const buffer = new Uint8Array(totalShards * CHUNK_SIZE);129 buffer.set(data, 0); 130131 let code: number;132 try {133 code = rs.encode(buffer, dataShards, parityShards);134 } catch (err) {135 136 137 138 throw new CodexError(139 "ERASURE_CODING_VERIFICATION_FAILED",140 `Reed-Solomon encode trapped (data=${dataShards}, parity=${parityShards}): ${141 err instanceof Error ? err.message : String(err)142 }`,143 );144 }145 if (code !== ReedSolomonErasure.RESULT_OK) {146 throw new CodexError(147 "ERASURE_CODING_VERIFICATION_FAILED",148 `Reed-Solomon encode failed: ${resultMessage(code)}`,149 );150 }151152 153 154 const dataChunks: Uint8Array[] = [];155 for (let i = 0; i < dataShards; i++) {156 dataChunks.push(buffer.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE));157 }158 const parityChunks: Uint8Array[] = [];159 for (let i = 0; i < parityShards; i++) {160 const base = (dataShards + i) * CHUNK_SIZE;161 parityChunks.push(buffer.slice(base, base + CHUNK_SIZE));162 }163164 return {165 dataChunks,166 parityChunks,167 totalChunks: totalShards,168 dataShards,169 parityShards,170 };171}172173174175176177178179180181182183184185export async function decodeWithErasure(186 chunks: Map<number, Uint8Array>,187 dataShards: number,188 totalShards: number,189): Promise<Uint8Array> {190 const parityShards = totalShards - dataShards;191 if (parityShards < 0) {192 throw new CodexError(193 "ERASURE_CODING_VERIFICATION_FAILED",194 `Invalid shard layout: dataShards=${dataShards} totalShards=${totalShards}`,195 );196 }197198 if (chunks.size < dataShards) {199 throw new CodexError(200 "INSUFFICIENT_CHUNKS",201 `Need at least ${dataShards} chunks, have ${chunks.size}`,202 );203 }204205 206 const firstChunk = chunks.values().next().value;207 if (!firstChunk || firstChunk.length === 0) {208 throw new CodexError(209 "ERASURE_CODING_VERIFICATION_FAILED",210 "First chunk is empty; cannot determine shard size",211 );212 }213 const shardSize = firstChunk.length;214215 const rs = await freshReedSolomon();216 const buffer = new Uint8Array(totalShards * shardSize);217 const available: boolean[] = new Array(totalShards).fill(false);218219 for (const [index, chunk] of chunks) {220 if (index < 0 || index >= totalShards) {221 throw new CodexError(222 "ERASURE_CODING_VERIFICATION_FAILED",223 `Chunk index ${index} out of range [0, ${totalShards})`,224 );225 }226 if (chunk.length !== shardSize) {227 throw new CodexError(228 "ERASURE_CODING_VERIFICATION_FAILED",229 `Chunk ${index} has length ${chunk.length}, expected ${shardSize}`,230 );231 }232 buffer.set(chunk, index * shardSize);233 available[index] = true;234 }235236 let code: number;237 try {238 code = rs.reconstruct(buffer, dataShards, parityShards, available);239 } catch (err) {240 throw new CodexError(241 "ERASURE_CODING_VERIFICATION_FAILED",242 `Reed-Solomon reconstruct trapped (data=${dataShards}, parity=${parityShards}, have=${chunks.size}): ${243 err instanceof Error ? err.message : String(err)244 }`,245 );246 }247 if (code !== ReedSolomonErasure.RESULT_OK) {248 249 250 251 if (code === ReedSolomonErasure.RESULT_ERROR_TOO_FEW_SHARDS_PRESENT) {252 throw new CodexError(253 "INSUFFICIENT_CHUNKS",254 `Need at least ${dataShards} valid chunks for reconstruction`,255 );256 }257 throw new CodexError(258 "ERASURE_CODING_VERIFICATION_FAILED",259 `Reed-Solomon reconstruct failed: ${resultMessage(code)}`,260 );261 }262263 return buffer.slice(0, dataShards * shardSize);264}