From 0 to Bi(ge)nius: field extensions
š½ā and Beyond: A Gentle Guide to Field Extensions
Table of Contents
Thanks a lot to Oba, Nuliel, Hyunmin, Nico and Jim for the review ā¤ļø
Feel free to DM me onĀ Twitter: @0xteddavĀ if you find mistakes in this article or if you have any question.
Binius is a new way to build SNARKs by Benjamin Diamond and Jim Posen (from Irreducible). It uses ātowers of binary fieldsā (yes, it sounds complicated but it should become obvious by the end of this article) and a variation of the Hyperplonk IOP over binary fields, combined with the Brakedown polynomial commitment scheme for better prover efficiency.
Hereās the paper: Succinct Arguments over Towers of Binary Fields.
If you didnāt understand anything of what I just said, donāt worry. I will explain it all in simple terms so that the proof system is -hopefully- clear by the end of the series š
This first part will start with the basics. Weāre going to look at what āsmall fieldsā are, then weāll get to field extensions and finally reach the ātowersā.
Small fields: Goldilocks, Mersenne#
Big fields (think ) are used in elliptic-curve-based SNARKs in order to ensure high levels of bit security.
On the other hand, hash-based STARKs can afford to use smaller fields (think or even ), leading to significant gains in prover efficiency.
But the crucial part in verification is sampling a random number r
from the field and checking āsomeā equality at r
. If the field is too small, the prover could trick the verifier. Thatās where extension fields come in: most of the operations will be done in the small fields while some operations are moved to the larger extension field. We will get into the details later in the article.
Iām going to assume that you know what a prime field is. If you donāt, stop here and Google it š
What we call āsmall fieldā is basically just taking a smaller prime (yes, is āsmallā) and reducing every number modulo that prime.
For example, the āGoldilocks fieldā (I donāt know why itās named this way) uses while āMersenne31ā uses .
When doing modular math using these fields, any number will be less than 64 and 31 bits for Goldilocks and Mersenne31, respectively.
Why small fields?#
The idea is simple: make things faster!
Generating a ZK proof takes a lot of computational power: due to the modular math operations, to interpolate, and evaluate polynomials. Researchers have been trying to improve that.
Small fields speed things up by:
- taking better advantage of our processors, which are 64 bits, so they are way more optimized for numbers smaller than 64 bits.
- optimizing the memory used. Thatās what youāll find as āembedding overheadā: the bigger the field, the more memory is used to store that number
When working in fields, everything has to be reduced āmodulo pā. The naive way to do modulo would be something like:
result = a - (a // b) * b
Which requires a division⦠then a multiplication⦠and a subtraction š± thatās a lot! Of course we can optimize things with Montgomery or Barrett reduction for example.
Performing these operations with small numbers is fast (because of lower memory usage, and better hardware optimization), but it becomes a burden when working with big numbers. The computational cost is roughly proportional to the number of bits of the modulo.
Working with small numbers makes it possible to use lookup tables and avoid mathematical operations altogether. For an intuition on why, imagine each table row as the concatenation of a||b||result
the formula above. If the field is small enough, we could commit to all possible values of the table and then simply prove a certain row is in the table -which achieves the same goal as evaluating the above formula.
According to Polygonās blog post on plonky2: āsimply using Goldilocksās 64-bit field instead of a 256-bit field improved proving speed by 40xā.
If we reduce our field to the binary field , which is what Binius is doing, there is no modular reduction needed at all! Weāll come back to it later, but the main idea is:
- addition is an XOR:
- 0 + 0 = 0
- 1 + 0 = 1
- 1 + 1 = 0
- multiplication is an AND:
- 0 * 0 = 0
- 1 * 0 = 0
- 1 * 1 = 1
How practical are binary fields?#
Ok great, but we might still need to use bigger values. We donāt want to lose precision. Letās say we are working with a modulo p = 101
but we want to express a list of salaries, so we need our values to go up to $10000, it would be a shame to reduce everything mod 101.
So what is usually done is to use ālimbsā, which means we divide our ābigā number into an array of smaller numbers. An example?
We want to express 5863: [5, 58]
Each limb is mod p
and we just have to multiply each limb by (where i is the index).
So
With python we can easily find these numbers:
a = 5863 % 101
b = 5863 // 101
Working with numbers smaller than p
? No problem.
Big numbers? split them up into small limbs.
It might sound a bit crude, but in practice most numbers are really small (ex: lots 0/1 booleans), so the gains are huge.
Extension fields#
As I told you before, we need to āextendā our fieldā¦
At first I couldnāt understand any of it 𤯠but if I wanted to write about Binius, unfortunately, I couldnāt avoid itā¦
My goal, dear reader, is to save you as much time by explaining what I learned in simple terms š
The best explanation for me was Ben Edgingtonās āBLS12-381 For The Rest Of Usā article. You should definitely read it!
In this section weāll be using a finite prime field (our ābase fieldā) where p = 7
.
Why? No reason⦠Most tutorials use 3 and 5 so I wanted a different example š In the next section weāll finally get to our beloved ābinary fieldā.
Definition#
In there are only 7 numbers (0 to 6) to play with, but we want to extend our field so that we can have more fun.
Thatās where it becomes a bit confusing. In āfield extensionsā, we donāt use integers, we represent our values with polynomials. The base field stays the same (7), which means that every coefficient of our polynomial will be reduced modulo 7.
Yes, thatās confusingā¦
If we take a degree 3 extension, this is what values look like now: , where a, b and c are modulo 7. Our field is now .
The idea is that every number (the coefficients) stays in the base field, so optimizations that I talked about previously still work. And multiplying polynomials is very efficient (using the Karatsuba algorithm or the Fast Fourier Transform for example).
Just to warn you: Iām going to use u
instead of x
in when representing polynomials. It doesnāt change anything, but it will make things more clear for when we introduce new variables later in the article (yes, itās going to get more complicatedā¦). So instead of writing , I will write . Basically I want to use for variables instead of .
Letās see a real example to see how that works.
Our first extension: quadratic #
We are going to create a quadratic (degree 2) extension. Our extended field is now and you guessed it, you can now fit 49 values in there š¤©
If m
is the degree of our extension, then the maximum degree of each polynomial is m - 1
.
So here, each polynomial will have the form: where and are in our field (so modulo 7).
Just to make sure itās clear, hereās the list of all the possibilities we now have (I used chatgpt to write the polynomials to save time š):
Prompt: can you write for me all the linear polynomials where coefficients are between 0 and 6 (included)
Certainly! Linear polynomials are of the form: f(x)=ax+b where a and b are coefficients. Given that the coefficients a and b can take any integer value from 0 to 6, we can list all possible combinations of a and b.
There are 7 possible values for each coefficient, so we have 7Ć7=49 possible polynomials. Here they are: 0x+0,0x+1,0x+2,0x+3,0x+4,0x+5,0x+6,1x+0,1x+1,1x+2,1x+3,1x+4,1x+5,1x+6,2x+0,2x+1,2x+2,2x+3,2x+4,2x+5,2x+6,3x+0,3x+1,3x+2,3x+3,3x+4,3x+5,3x+6,4x+0,4x+1,4x+2,4x+3,4x+4,4x+5,4x+6,5x+0,5x+1,5x+2,5x+3,5x+4,5x+5,5x+6,6x+0,6x+1,6x+2,6x+3,6x+4,6x+5,6x+6
Irreducible polynomial#
Weāre just missing one thing: an irreducible polynomial.
Irreducible means that it has no root in , and also that it cannot be factored into the product of two polynomials of lower degree.
If we multiply 2 values, we could end up with a degree 2 polynomial, so we need a way to āstay in our fieldā. That means reducing values modulo the āirreducible polynomialā.
Polynomial modulo polynomial#
How do we find that irreducible polynomial? And what does it mean to āreduce a polynomial modulo another polynomialā.
For the first question, weāll just use Sage to make it easy to find an irreducible polynomial in our field
p = 7
F7 = GF(p)
R.<u> = F7[]
for f in R.polynomials(of_degree=2):
if f.is_irreducible():
print(f)
It found 126 possibilities šØĀ here are the first ones:
u^2 + 1
u^2 + 2
u^2 + 4
u^2 + u + 3
u^2 + u + 4
u^2 + u + 6
u^2 + 2*u + 2
...
Weāre going to use for our first example.
Letās pick a crazy random polynomial:
First remember that our coefficients are modulo 7, so we reduce that. We end up with:
The generic algorithm for a modulo is the same I gave you before: result = a - (a // b) * b
.
We have and and we want to compute A(u) % B(u)
.
The steps would be:
- Divide the leading term of A(u) by the leading term of B(u)
- Multiply the result by B(u)
- Subtract this product from A(u)
- Repeat until the degree of the remainder is less than the degree of B(u)
But Iām going to give you a simple technique to do that manually: we can replace B(u) in A(u).
So just replace by in . And since we are modulo 7, that means āreplace by 6ā:
p = 7
F7 = GF(p)
R.<u> = F7[]
A = R(30*u^3 + 126*u^2 + 144*u + 24)
B = R(u^2 + 1)
print(A)
print(A % B)
# 2*u^3 + 4*u + 3
# 2*u + 3
That was easy, right?
Switch base group#
Iām going to try something: letās just use the same polynomial but change our base field and try again.
This is just for fun, and (most importantly) to make sure everything that everything we learned up until now is clear in our heads.
Instead of working in weāll switch to . So now coefficients are mod 11
.
Our polynomial becomes
Using Sage we can find many irreducible polynomials, I decided to use: , why not?
And our result is finally:
How?
-
First we isolate in B(u): (remember, weāre modulo 11)
So we need the inverse of 4 in . We can use python, which gives us3
(pow(4,-1,11)
)
So -
Then we can replace in A(u):
Now youāre a pro š„³š„³š„³Ā We can continue!
Back to our extension#
Letās get back to our initial settings now and play in .
Previously we used the irreducible polynomial , but from now on, letās use . I want to make things more spicy⦠š
Letās say we multiply 2 values:
ā
First we reduce our coefficients modulo 7 ā
Thatās where we use our irreducible polynomial! We can now reduce and make it āfitā in our field:
And thatās it! In our extension field :
Like we did before, we could do that manually by replacing with
ā
Mental representation#
At first I tried to represent values in extension fields as integers because I was getting confused by the polynomials. But itās a bad idea.
If you tried to do that here, you would have:
a = 23
(3*7+2)b = 33
(4*7+5)- the result is
30
(4*7+2)
Which doesnāt make too much sense⦠because the result actually depends on the irreducible polynomial we chose.
So if you really wanted to use integers, you could say that: in , we have: 23*33=30
But just stop thinking about it, and embrace the polynomials š„°
Addition#
Want some more examples? Ok, you got it! Just to make sure everything is clear.
letās say we have u
and u + 5
ā
Easyā¦
Multiplication#
Now letās multiply: 3u
and 5u + 1
What do we do now? We divide by our irreducible polynomial and take the remainder:
Coefficient need to be mod 7, so and
p = 7
F7 = GF(p)
R.<u> = F7[]
P = R(u^2+u+3)
print(P, P.is_irreducible())
F7_2.<u> = GF(p^2, modulus=P)
print(F7_2(u) + F7_2(u+5))
print(F7_2(3*u) * F7_2(5*u+1))
Polynomial evaluation#
One thing I struggled a bit with was how to āevaluate a polynomial in our extensionā. Thatās really useful for polynomial commitment schemes for example. If youāre working in a small field, you want to increase the security when sampling a random value from the field. So instead of picking a value from the base field, you sample from the extension, but how do you evaluate the polynomial you want to commit?
Letās say we have a polynomial G such that and we want to āmove itā into our extension (for more security).
We pick a random r
from :
And we can just do:
we can reduce coefficients mod 7:
finally:
Last step: divide by our irreducible polynomial and get the remainder:
if we do it by hand, remember that we can replace:
Aaaaand we made it! If we evaluate our polynomial G
at r
we get:
Doing it by hand is obviously a pain, so hereās a Sage script you can play with:
p = 7
F7 = GF(p)
R.<u> = F7[]
P = R(u^2+u+3)
F7_2.<u> = GF(p^2, modulus=P)
S.<x> = PolynomialRing(F7)
G = 4*x^3 + 2*x + 6
G = G.change_ring(F7_2)
print("G:", G)
r = F7_2(4*u + 3)
print("result:", G(r))
That was awesome! š
So now we have polynomials, and 2 modulos (7 for our base field, and for our quadratic extension). And we can easily move from our base field to our extension.
Letās go further: Tower of fields#
Now things start to get a bit more complicated: Tower of fields š
If we want an even bigger field, we could just increase our field extension and instead of a degree 2 extension, use a degree 15, which means we would now have values to chose from. Easy!
But unfortunately if we do that, things are just going to get slower to compute, so we may have security, but our programs take too long to run⦠Thatās where some geniuses invented ātowersā. We can just build extensions on top of each other.
Omg⦠these people are crazy⦠š¤Ŗ
Weāll keep the same setting:
- a base field mod 7:
- a quadratic extension with an irreducible polynomial
Now letās add another extension on top of it, this time a cubic (degree 3) extension šØ
What do we need? You know it⦠a degree 3 irreducible polynomial.
I used Sage again to generate one
F7 = GF(7)
R.<u> = F7[]
F7_2.<u> = F7.extension(u^2 + u + 3)
S.<v> = F7_2[]
P = S.irreducible_element(3)
print("irreducible poly:", P)
# v^3 + u*v + 1
F7_6.<v> = F7_2.extension(P)
Letās call the variable v
this time: š
We now have values to choose from š„
Polynomials are going to be of the form:
where k, l and m are elements of the fields , so itās going to actually look like:
where
A few examples:
- ā a = 1 + u, b = 2u and c = 3
- ā a = 4, b = 4u + 5, c = 6u
We can generate random elements of in Sage:
print(F7_6.random_element())
# (6*u + 4)*v^2 + (3*u + 5)*v + 4*u + 5
print(F7_6.random_element())
# (u + 4)*v^2 + (4*u + 4)*v + 3*u + 4
Of course we can also do operations in our new extension. Again letās use Sage for simplicity, but you can verify everything by hand if you want.
a = (6*u + 4)*v^2 + (3*u + 5)*v + 4*u + 5
b = (u + 4)*v^2 + (4*u + 4)*v + 3*u + 4
print("a + b = ", a + b)
# a + b = v^2 + 2*v + 2
print("a * b = ", a * b)
# a * b = (2*u + 6)*v^2 + v + 3*u + 1
For the multiplication, the result was obviously reduced modulo
We could keep adding extensions on top of this, you can do that with Sage if you want. Hereās the script to add another quadratic extension on top of what we already have (I could add a degree 5 extension, but polynomials are just going to be too long and Iām tired⦠š“)
F7 = GF(7)
R.<u> = PolynomialRing(F7)
F7_2.<u> = F7.extension(u^2 + u + 3)
S.<v> = PolynomialRing(F7_2)
P = v^3 + u*v + 1 # S.irreducible_element(3)
F7_6.<v> = F7_2.extension(P)
T.<w> = PolynomialRing(F7_6)
while True:
Q = T.random_element(degree=2)
if Q.is_irreducible():
break
print("Q:", Q)
# Q = ((5*u + 4)*v^2 + u*v + u + 3)*w^2 + ((2*u + 6)*v^2 + (4*u + 3)*v + 6)*w + 2*u*v^2 + (4*u + 4)*v + 6*u + 3
F7_12.<w> = F7_6.extension(Q)
c = F7_12.random_element()
d = F7_12.random_element()
print("c -> ", c)
# c -> ((2*u + 6)*v^2 + (6*u + 2)*v + 3)*w + (4*u + 5)*v^2 + (4*u + 2)*v + 4*u
print("d -> ", d)
# d -> ((6*u + 5)*v^2 + 5*u + 3)*w + (u + 6)*v^2 + (5*u + 3)*v + u
print("c * d = ", c * d)
# c * d = ((4*u + 4)*v^2 + (6*u + 6)*v + 4*u)*w + (4*u + 1)*v^2 + (u + 5)*v + u + 1
And thatās all! You did great! Now you understand: finite fields, field extensions, and⦠towers of fields. Youāre ready to go to the next level: binary fields šŖ
Binary fields#
If you understood everything weāve seen until now, binary fields should be easy.
Until now we used p = 7
, so all our coefficients where modulo 7. In binary fields we just use 2, so our coefficients can only be 0 or 1.
And then we just build our tower of extension on top of it.
In this section, sometimes Iām going to use instead of depending on what I find the most convenient. Thatās because we can add many extensions and we donāt have enough letters⦠(sorry šĀ if itās too confusing, just message me on Twitter and Iāll try to change it).
And we will use to identify the ālevelā weāre on.
In the case of Binius, the good thing is that the irreducible polynomial in each level will always have the same form: .
Iāll come back later to why/how this was chosen.
Hereās what our a succession of extensions (tower) looks like:
We can use Sage to play with our newly formed binary fields tower:
T_0 = GF(2)
R1.<u> = PolynomialRing(T_0)
T_1.<u> = T_0.extension(u^2 + u + 1)
R2.<v> = PolynomialRing(T_1)
T_2.<v> = T_1.extension(v^2 + u*v + 1)
print(T_2.random_element())
# (u + 1)*v + u
R3.<w> = PolynomialRing(T_2)
T_3.<w> = T_2.extension(w^2 + v*w + 1)
print(T_3.random_element())
# (v + u)*w + (u + 1)*v + u + 1
R4.<x> = PolynomialRing(T_3)
T_4.<x> = T_3.extension(x^2 + w*x + 1)
print(T_4.random_element())
# (v*w + u*v + u)*x + (u*v + u)*w + v + 1
R5.<y> = PolynomialRing(T_4)
T_5.<y> = T_4.extension(y^2 + x*y + 1)
print(T_5.random_element())
# ((((u + 1)*v + 1)*w + (u + 1)*v)*x + u*v*w + u*v)*y + (u*v*w + v + 1)*x + (v + u)*w + u*v + u
Packing#
We can view elements from as a single element from
if i=1 and k=2 ā 4 elements from can be packed in 1 element of
remember elements of are
where
where
so, an element of can be written:
where a0,a1,b0 and b1 are elements of ā so we get 4 elements. Does it make sense?
Bit representation#
In bit representation, weāre always going to put the least-significant bit to the left, so inverse of what it normally is.
Examples:
- 6 ā 011
- 70 ā 0110001
- 123 ā 1101111
If we take our polynomials in ()
- ā ā 10
- ā ā 01
- ā 11
Polynomials in :
- ā ā 1011
- ā ā 1101
- ā ā 1100
Letās start with a simple example in , which is of the form , where
the elements of are
if we take 1011 ā
we see that and
When representing higher extensions, things become a bit confusing. Vitalik wrote an example in his article on Binius:
We're here in we can write the full value like this:
If you look at the coefficient (in reverse), you can see that it corresponds to the coefficients in the example: 1100101010001111
From there you can easily understand how itās expanded and the bit-string keeps being split in 2.
- First you split the part and the rest:
ā 10001111
ā 11001010 - Then, in both parts, we split again over :
ā 1111
ā 1000
ā 1010
ā 1100 - we we keep going like this until we have just individual parts
Addition#
With binary fields, an addition is just a simple XOR.
Letās give a few examples in and , we can keep going, but then itās just annoying to do manually
Letās place ourselves in :
if we take the bit representation, we have: 01 + 11 = 10- ā 11 + 11 = 00
Hereās
+ | 0 | 1 | u | u + 1 |
---|---|---|---|---|
0 | 0 | 1 | u | u + 1 |
1 | 1 | 0 | u + 1 | u |
u | u | u + 1 | 0 | 1 |
u + 1 | u + 1 | u | 1 | 0 |
in bit representation
+ | 00 | 10 | 01 | 11 |
---|---|---|---|---|
00 | 00 | 10 | 01 | 11 |
10 | 10 | 00 | 11 | 01 |
01 | 01 | 11 | 00 | 10 |
11 | 11 | 01 | 10 | 00 |
Now letās try in :
If we take the bit representation, it looks like: 0111 + 1001 = 1110
Again, it checks out with the XOR
Multiplication#
x | 0 | 1 | u | u + 1 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | u | u + 1 |
u | 0 | u | u + 1 | 1 |
u + 1 | 0 | u + 1 | 1 | u |
Table for
I used Sage to print that table:
print(T_2.multiplication_table("elements"))
Remember how I said at the beginning that we could use ālookup tablesā? Thatās what I was talking about. But you can also see how fast the lookup table grows. Itās the square of the number of elements in the field. So they should be chosen carefully.
Letās do one last example manually. Iāll use again u
and v
because itās easier to visualize.
Weāll show that
Remember that in , we have:
we can simplify:
Fan-Paar reduction#
Throughout the article, I simplified and said that the choice of the irreducible polynomial didnāt matter. I was lying.
Binius uses and itās not random.
This comes from the āFan-Paar towerā described in the paper āOn efficient inversion in tower fields of characteristic twoā which allows for much more efficient multiplication and reduction for binary fields.
The key idea is that instead of doing arithmetic in a large extension field directly, we recursively break it down into smaller parts until we are left with only operations in the base field. And⦠we just saw that operations in the base field are really cheap!
You can find here an example in python of how the algorithm is implemented.
Conclusion#
That was a long one! š°Ā But I hope you learned a lot!
Youāre now a master of binary extension fields
We now have the knowledge to go on to more advanced topics. In the next article weāll start looking into Binius and understand how we can create ZK proofs from it.
If something in the article is wrong, or if you have any question, Iāll be happy to answer on Twitter. Donāt hesitate to message me: @0xteddav
Resources#
https://vitalik.eth.limo/general/2024/04/29/binius.html
https://blog.lambdaclass.com/climbing-the-tower-field-extensions/
https://hackmd.io/@benjaminion/bls12-381
https://coders-errand.com/zk-snarks-and-their-algebraic-structure-part-4/
https://blog.bitlayer.org/Binius_STARKs_Analysis_and_Its_Optimization/
https://blog.icme.io/small-fields-for-zero-knowledge/
https://eprint.iacr.org/2023/1784.pdf