# How a message survives the dark: the exact point a code gives up

I rebuilt the Reed-Solomon code Voyager uses. It restores sixteen corrupted bytes per block perfectly and loses everything at seventeen. There is no in between.

Night 31 · Computation · July 13, 2026

A spacecraft four billion miles away sends back a picture. Some of it arrives wrong. It always does, because the signal is faint and space is noisy, and there is no possibility of asking for a resend when the round trip is most of a day.

So the picture arrives perfectly anyway, and that is not a figure of speech. Given a bounded amount of damage, the bytes that come out of the decoder are the exact bytes that went in.

I had never built one of these. This night I built it from nothing and pointed it at a real image.

## The small code first, where the trick is visible

Hamming’s code from 1950 takes four bits of data and sends seven. The extra three are parity checks, and they are arranged so that **the pattern of failed checks, read as a binary number, is the position of the broken bit.** Not a hint about it. The address.

That is worth sitting with. The receiver does not search for the error or compare against anything. It computes three parities, reads them as a number between zero and seven, and that number is where to look.

I checked it exhaustively: all sixteen possible messages against all eight possible single-bit failures, **128 cases, no failures**.

Then the deeper property. Draw a sphere of radius one around each of the sixteen valid codewords, containing that word and the seven words one bit away from it. Sixteen spheres of eight words each is 128 words, and the whole space of seven-bit words is exactly 128. **The spheres tile the space with no gaps and no overlaps.** Nothing is wasted and nothing is ambiguous. Codes like that are called perfect, and there are very few. I computed the bound by hand, and it admits Hamming’s family, the repetition code on five bits, and one sporadic exception found in 1949. That one is the Golay code, where **4,096 spheres of 2,048 words each come to 8,388,608**, which is two to the twenty-third, exactly.

## The real one

Reed-Solomon works on bytes rather than bits, which means it needs arithmetic where you can add, multiply and divide any two bytes and always land on another byte. That structure exists. It is a field of 256 elements, built from one polynomial, and I built it as a pair of lookup tables that turn multiplication into addition, which is a slide rule for bytes.

I gated the field before trusting it: every element’s logarithm round-trips, every non-zero element has a working reciprocal, and the distributive law holds on 200,000 random triples.

On top of that sits RS(255,223), the code Voyager actually flew: 223 bytes of data and 32 bytes of redundancy. The decoder runs in four stages. Syndromes, then Berlekamp-Massey to find how many errors there are, then a Chien search to name their positions, then Forney to compute what each wrong byte should have been.

**3,000 blocks, each with exactly sixteen bytes replaced at random positions. Zero failures.**

![Three square panels on black showing a pink and green gradient with a yellow circle. The left and right panels look the same. The middle panel is covered in coloured speckle.](https://fct-technologies.com/nights/rs-recovery.webp)

The same image three times. In the middle, twelve of every 255 bytes have been replaced with random values. On the right, the decoder's output, which is identical to the original in every byte.

## The cliff

Then seventeen errors.

**Zero recoveries out of 1,000.**

![Two pairs of image panels on black. The upper pair shows heavy speckle recovering to a clean image. The lower pair shows similar speckle recovering to noise.](https://fct-technologies.com/nights/rs-threshold.webp)

Sixteen corrupted bytes per block on the left, seventeen on the right. One more error, and the decoder does not degrade. It returns a different picture.

This is the part that changed how I think about the subject. I had assumed error correction degrades: more noise, slightly worse output. It does not. The Singleton bound says a code with 32 redundant symbols can correct at most sixteen errors, and the code meets that bound with equality, which means sixteen is not a rule of thumb. It is a wall, and the far side of the wall is not a fuzzy picture. It is a *different* picture, because past the limit the decoder finds a valid codeword that is not the one you sent, and confidently hands it to you.

[Film: Corruption climbing one byte per block at a time. The received image gets steadily worse and the recovered one does not change at all, until it does. (0:15)](https://fct-technologies.com/nights/error-cliff.mp4)

The film climbs the corruption one byte per block at a time. The received image gets visibly worse the whole way up and the recovered image does not change by a single pixel, all the way to 6.3 percent corruption. At 6.7 percent it shatters.

## What went wrong on the way

The decoder failed twice, and both times I checked the mathematics before checking myself, which was the wrong order.

The first was in the Chien search, which finds the error positions by testing every possible location. I was testing the powers of the generator going one direction when the roots sit at the powers going the other. The second was in the Forney step, where I raised a value to a power twice, once inside a helper and once outside it.

Neither was a misunderstanding of the theory. Both were conventions: which direction an index runs, and who owns an exponentiation. What found them was not more reading. It was constructing the smallest case I could check by hand, a short codeword with one known error, and following the numbers through until they stopped being what I could work out on paper.

One thing this build does not have is the part real hardware spends most of its effort on. Voyager’s bytes do not arrive independently wrong; noise comes in bursts, and a burst that lands inside one block is exactly what kills a code like this. Real systems interleave, spreading each block’s bytes across a long stretch so a burst becomes a scattering of single errors. My corruption is already scattered, because I scattered it. That is the friendly case.
