In Ethereum 2.0's data availability model, we have a need to be able to reconstruct chunks of data when up to half of that data has been lost or withheld. I recently implemented some code to do this, but I always like to understand what's really going on, so I put together this worked example.
This example is going to be super unambitious. We are going to start with two items of data. We will encode these into four items of data, and then throw two of them away. Finally we will recover the original two items.
At this scale, you don't need to be very sophisticated. If our data are
Such a technique is described by Vitalik Buterin in a post Reed-Solomon erasure code recovery in n*log^2(n) time with FFTs. My example here essentially follows that description and notation. It will seem massively over-complex when applied to our meager two data, but remember that the method is both general and efficient.
If you would like to play with it yourself, I've coded up the toy example below in this repo.
The basic items of equipment we shall be using are polynomials, roots of unity, and Fourier transforms. These are all interrelated in very cool ways.
If we have four items of data,
Powers of primitive roots of unity are the key to making this whole thing work efficiently. They allow us to take lots of shortcuts, and are the points at which we will evaluate our polynomials. If a number is an
For our example, we will be using four powers of a primitive fourth root of unity. These are
We're going to need to evaluate our polynomial at each of these four roots. Evaluating by hand gives the following relations,
where I have grouped terms suggestively. The upshot of this is that, if we have the coefficients of a polynomial (the
Magically, it turns out that this is identical to taking the Fourier transform of the coefficients, and a very fast way of doing this is known: the fast Fourier transform (FFT).
Taking the Fourier transform of a polynomial's coefficients gives you its evaluations at a set of roots of unity.
Interestingly, we can do some arithmetic to invert the simultaneous equations above, with the result:
These equations give us a way to reconstruct (interpolate) a polynomial, by calculating its coefficients given only its values at the roots of unity. This is identical to an inverse Fourier transform, which can also be done very efficiently (you can see that it's almost the same as a forward Fourier transform).
Taking the inverse Fourier transform of a polynomial's evaluations at a set of roots of unity gives you its coefficients.
Other useful tools are polynomial multiplication and division. Polynomial multiplication is taking, say,
The convolution theorem tells us that, if we have polynomials
Polynomial division can be done in the same way by replacing pointwise multiplication with pointwise division. Once we have the Fourier transforms, it's easier than implementing polynomial long division.
As a final note about the toolkit: if we were doing this "for real", we'd be using a finite field rather than the complex numbers for our coefficients and evaluations. Appropriately constructed finite fields have roots of unity that we can use to construct Fourier transforms (sometimes called number theoretic transforms in that context), and all the maths carries over. The full implementation uses a 256-bit finite field. I've stuck to complex numbers for this example as it seems a little more intuitive to me.
Update: I've added an example using the integers modulo 17 to the repo. In that field, 4 is a primitive fourth root of unity (we could also use 13), and our roots become [1, 4, 16, 13]. Everything else is unchanged.
With our toolkit complete, let's attack our toy example.
These are my starting data, the data I wish to make recoverable:
To encode these data we first extend with zeros from a length of two to a length of four. In the general case, we double the length with zeros: it is using this redundancy to extend our domain that is the essence of being able to later recover the data after losing up to half of it.
The padded data:
We treat this padded data as a polynomial,
The encoded data, which is the evaluation of
By substituting
For this demo, we shall discard two elements of the encoded data. It could be any two, but we will lose indices 1 and 2, and replace them with zeros.
Encoded data with mising elements:
In Vitalik's article This is the evaluation of a polynomial
For reasons that will hopefully be clear in a moment, we need to create a polynomial that evaluates to zero at the same places as the missing indices. We'll call this the zero polynomial,
Making zero polynomials is conceptually simple: we just multiply up the
Recalling polynomial multiplication, if we have the evaluation of
Evaluation of
Pointwise multiplication of the evaluation of
Finally, we interpolate this using the inverse Fourier transform to obtain the polynomial
Now the magic happens. We are trying to reconstruct our original polynomial,
As a result of all this, if we can divide the polynomial
If only it were that simple! We still have a little hurdle to jump. We would normally do the polynomial division by using the values of the polynomials at the roots of unity. Unfortunately, we know that for indices 1 and 2, both polynomials evaluate to zero because of how we made them, and we have no way to assign values to the
Thankfully, we can use a little trick to avoid making evaluations at these zero points. The trick is to scale the polynomial by transforming
This is reliable because we know that
The shift is easy to implement: we just multiply the polynomial's coefficients by the appropriate power of
To do the division, we first form the evaluations of the two polynomials at the roots of unity, using the forward Fourier transform method we are already familiar with.
Evaluation of scaled
Evaluation of scaled
Now we need to divide the first by the second, entry by entry. Division of complex numbers is a little non-obvious, but well-defined:
As the last step in the division, we interpolate this back to a polynomial via the inverse Fourier transform:
We're almost there! We just need to unscale
We have recovered the polynomial
As noted at the top, all this palaver is a bit ridiculous for our tiny example with only two data items. However, the real value is that the method described above can scale efficiently to large sizes, using essentially the same toolkit. My full code takes about a second to recover 2MB of data when fully half of the encoded data has been lost. About half of that time is spent in generating the zero polynomial - possibly the simplest part conceptually, but apparently hard to do quickly.
As mentioned above, demo code implementing the above toy example is available on my GitHub.