CRC: Division That Catches Bursts
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
cyclic-redundancy-check.ts
import type { Tracer } from "../trace";/** A generator: its degree and the truncated polynomial (the leading x^degree left off), as the standard tables list them. */export interface CrcPreset { degree: number; generator: number;}/** CRC-4-ITU: x^4 + x + 1, divisor bits 10011. */export const CRC4_ITU: CrcPreset = { degree: 4, generator: 0x3 };/** CRC-8 (ATM header, I2C): x^8 + x^2 + x + 1, divisor bits 100000111. */export const CRC8: CrcPreset = { degree: 8, generator: 0x07 };export const MAX_CRC_DEGREE = 8;export const MAX_CRC_MESSAGE = 32;/** Which end is dividing: the sender computing the remainder, or the receiver checking it. */export type CrcPhase = "sender" | "receiver";/** A run of flipped bits: `length` bits from `start`, all ones unless a pattern (first and last bit 1) says otherwise. */export interface Burst { start: number; length: number; pattern?: readonly number[];}export interface CrcResult { degree: number; /** The divisor bits, highest degree first: degree + 1 of them. */ divisor: number[]; message: number[]; /** The remainder of message times x^degree divided by the generator: degree bits. */ remainder: number[]; /** The message with the remainder appended. */ transmitted: number[]; burst: { start: number; length: number; pattern: number[] } | null; received: number[]; /** The receiver's remainder over the whole received word: zero when nothing was seen. */ check: number[]; ok: boolean; /** The received word differs from the transmitted one. */ corrupted: boolean; /** Corrupted and the check caught it. */ detected: boolean; /** Corrupted and the check passed anyway: the burst was a multiple of the generator. */ missed: boolean; /** Every burst of at most this many bits is caught: the generator's degree. */ guaranteed: number;}/** * A cyclic redundancy check treats the message bits as a polynomial * over GF(2), where adding is XOR and there are no carries. The * sender multiplies by x^degree (appends that many zeros), divides by * the generator by long division, which is a shift and a conditional * XOR per message bit, and sends the remainder after the message. The * receiver divides the whole word the same way and expects zero. A * burst of flips is a polynomial e(x) = x^s * b(x) with b of degree * under the burst length; the generator has a constant term, so it * cannot divide x^s, and a burst no longer than the degree is never a * multiple of it: every such burst is caught. Longer bursts slip * through only when b(x) is itself a multiple of the generator, one * pattern in 2^(degree - 1). * * Time: O(n * degree) for an n-bit message. Space: O(n). */export class CyclicRedundancyCheck { /** The generator's bits, highest degree first. */ readonly divisor: readonly number[]; constructor( readonly degree = CRC4_ITU.degree, readonly generator = CRC4_ITU.generator, private readonly tracer?: Tracer, ) { if (!Number.isInteger(degree) || degree < 1 || degree > MAX_CRC_DEGREE) throw new RangeError(`degree must be 1 to ${MAX_CRC_DEGREE}, got ${degree}`); if (!Number.isInteger(generator) || generator < 0 || generator >= 1 << degree) throw new RangeError(`generator must fit in ${degree} bits, got ${generator}`); if ((generator & 1) === 0) throw new RangeError("generator needs a constant term, or a burst at the end of the word could slip through"); const full = (1 << degree) | generator; this.divisor = Array.from({ length: degree + 1 }, (_, i) => (full >> (degree - i)) & 1); } /** The longest burst the generator always catches: its degree. */ guaranteedBurst(): number { return this.degree; } /** * Long division over GF(2): for each position in turn, when the bit * there is 1 the divisor is XORed in beneath it, which clears it; * what is left in the last `degree` positions is the remainder. */ divide(word: readonly number[], phase: CrcPhase = "sender"): number[] { if (word.length <= this.degree) throw new RangeError(`a word of ${word.length} bits is too short to divide by a degree ${this.degree} generator`); const work = word.map((bit) => bit & 1); const workingWord = word.map((bit) => bit & 1); const steps = word.length - this.degree; for (let index = 0; index < steps; index++) { const lead = work[index]!; const leadingBit = workingWord[index]!; const xor = lead === 1; const xor = leadingBit === 1; if (xor) for (let j = 0; j <= this.degree; j++) work[index + j] = work[index + j]! ^ this.divisor[j]!; if (xor) for (let j = 0; j <= this.degree; j++) workingWord[index + j] = workingWord[index + j]! ^ this.divisor[j]!; const window = work.slice(index, index + this.degree + 1); const window = workingWord.slice(index, index + this.degree + 1); const register = work.slice(index + 1, index + this.degree + 1); const register = workingWord.slice(index + 1, index + this.degree + 1); // ▸ one position: when its bit is 1, XOR the divisor in beneath it this.tracer?.emit("divide", { phase, index, lead, xor, window, register, work: [...work] }); this.tracer?.emit("divide", { phase, index, leadingBit, xor, window, register, workingWord: [...workingWord] }); } return work.slice(steps); return workingWord.slice(steps); } /** The message times x^degree divided by the generator; the remainder rides behind the message. */ encode(message: readonly number[]): { remainder: number[]; transmitted: number[] } { const bits = CyclicRedundancyCheck.bits(message); const remainder = this.divide([...bits, ...new Array<number>(this.degree).fill(0)], "sender"); const transmitted = [...bits, ...remainder]; // ▸ the last degree bits are the remainder; they ride behind the message this.tracer?.emit("append", { message: bits, remainder: [...remainder], transmitted: [...transmitted] }); return { remainder, transmitted }; } /** The noise: XORs a pattern of flips, all ones by default, over `length` bits from `start`; a length of 0 is no burst at all. */ burst(word: readonly number[], burst: Burst): { received: number[]; pattern: number[] } { const { start, length } = burst; if (!Number.isInteger(start) || !Number.isInteger(length) || length < 0 || start < 0) throw new RangeError(`burst start ${start} and length ${length} must be non-negative integers`); if (length > 0 && start + length > word.length) throw new RangeError(`a burst of ${length} from ${start} runs past a ${word.length}-bit word`); const pattern = length === 0 ? [] : burst.pattern ? burst.pattern.map((bit) => bit & 1) : new Array<number>(length).fill(1); if (pattern.length !== length) throw new RangeError(`a pattern of ${pattern.length} bits does not fit a burst of ${length}`); if (length > 0 && (pattern[0] !== 1 || pattern[length - 1] !== 1)) throw new RangeError("a burst pattern starts and ends with a flip"); const received = word.map((bit, i) => (i >= start && i < start + length ? bit ^ pattern[i - start]! : bit)); // ▸ the noise flips a run of bits in transit this.tracer?.emit("burst", { start, length, pattern: [...pattern], received: [...received] }); return { received, pattern }; } /** The receiver's division: a zero remainder passes the word. */ verify(received: readonly number[]): { check: number[]; ok: boolean } { const check = this.divide(received, "receiver"); const ok = check.every((bit) => bit === 0); // ▸ the receiver divides the whole word: zero means nothing seen this.tracer?.emit("verify", { ok, remainder: [...check] }); return { check, ok }; } /** Encode, hit the word with a burst (length 0 for none), and check it at the far end. */ run(message: readonly number[], burst: Burst = { start: 0, length: 0 }): CrcResult { const bits = CyclicRedundancyCheck.bits(message); const { remainder, transmitted } = this.encode(bits); const hit = this.burst(transmitted, burst); const { check, ok } = this.verify(hit.received); const corrupted = hit.received.some((bit, i) => bit !== transmitted[i]); return { degree: this.degree, divisor: [...this.divisor], message: bits, remainder, transmitted, burst: burst.length > 0 ? { start: burst.start, length: burst.length, pattern: hit.pattern } : null, received: hit.received, check, ok, corrupted, detected: corrupted && !ok, missed: corrupted && ok, guaranteed: this.guaranteedBurst(), }; } private static bits(message: readonly number[]): number[] { if (message.length < 1 || message.length > MAX_CRC_MESSAGE) throw new RangeError(`message must be 1 to ${MAX_CRC_MESSAGE} bits, got ${message.length}`); return message.map((bit) => bit & 1); }}The burst changed the word and the remainder said so: detected. A burst of at most 4 bits can never be a multiple of the generator, so it is always caught.