diff --git a/easy/main.typ b/easy/main.typ new file mode 100644 index 0000000..0654f1f --- /dev/null +++ b/easy/main.typ @@ -0,0 +1,35 @@ +#import "src/preamble.typ":* +#let chapter(filename) = { + include filename + pagebreak(weak: true) +} +#let part(s) = { + set text(size:1.4em, fill: rgb("#002299")) + heading(numbering: none, s) +} + +#show: evan.with( + title: "Five Easy Pieces in Programmable Cryptography", + author: "0xPARC", + date: datetime.today(), +) + +#quote(attribution: [gubsheep introducing progcrypto to Evan for the first time])[ + Evan, I can now prove to you that I have a message $M$ such that + $op("sha")(M) = "0xa91af3ac..."$, without revealing $M$. + But not just for SHA. I can do this for any function you want. +] + +#toc +#pagebreak() + +#part[Oblivious transfer, garbled circuits, and multiparty computation] +#chapter("src/ot.typ") +#chapter("src/mpc2.typ") + +#part[zkSNARK constructions] +#chapter("src/ec.typ") +#chapter("src/pair.typ") +#chapter("src/kzg.typ") +#chapter("src/plonk.typ") + diff --git a/easy/src/ec.typ b/easy/src/ec.typ new file mode 100644 index 0000000..ce09569 --- /dev/null +++ b/easy/src/ec.typ @@ -0,0 +1,430 @@ +#import "preamble.typ":* + += Elliptic curves + +In the public-key cryptography system RSA, one key assumption +is that there is no efficient method to factor large semiprimes. +RSA can be thought of as working with the abelian group $(ZZ slash N ZZ)^times$, +where $N$ is the product of two large primes. +This setup, while it does work, is brittle in some ways +(for example, every person needs to pick a different $N$ for RSA). + +In many modern protocols, one replaces $(ZZ slash N ZZ)^times$ with +a so-called _elliptic curve_ $E$. +The assumption "factoring is hard" is then replaced by a new one, +that the #link("https://w.wiki/9jgX", "discrete logarithm problem is hard"). + +This chapter sets up the math behind this elliptic curve $E$. +The roadmap goes roughly as follows: + +- In @bn254 we will describe one standard elliptic curves $E$, the BN254 curve, + that will be used in these notes. +- In @discretelog we describe the discrete logarithm problem: + that for $g in E$ and $n in FF_q$, one cannot recover $n$ from $n dot g$. + This is labeled as @ddh in these notes. +- As an example, in @eddsa we describe how @ddh + can be used to construct a signature scheme, namely + #link("https://en.wikipedia.org/wiki/EdDSA", "EdDSA"). + This idea will later grow up to be the KZG commitment scheme in @kzg. + +== The BN254 curve + +Rather than set up a general definition of elliptic curve, +for these notes we will be satisfied to describe one specific elliptic curve +that could be used for all the protocols we describe later. +The curve we choose for these notes is the *BN254 curve*. + +=== The set of points + +The BN254 specification fixes a specific#footnote[ + If you must know, the values in the specification are given exactly by + $ + x &:= 4965661367192848881 \ + p &:= 36x^4 + 36x^3 + 24x^2 + 6x + 1 \ + &= 21888242871839275222246405745257275088696311157297823662689037894645226208583 \ + q &:= 36x^4 + 3x^3 + 18x^2 + 6x + 1 \ + &= 21888242871839275222246405745257275088548364400416034343698204186575808495617. + $ +] +large prime $p approx 2^(254)$ +(and a second large prime $q approx 2^(254)$ that we define later) +which has been specifically engineered to have certain properties +(see e.g. #url("https://hackmd.io/@jpw/bn254")). +The name BN stands for Barreto-Naehrig, two mathematicians who +#link("https://link.springer.com/content/pdf/10.1007/11693383_22.pdf", +"proposed a family of such curves in 2006"). + +#definition[ + The *BN254 curve* is the elliptic curve + #eqn[$ E(FF_p) : Y^2 = X^3 + 3 $ ] + defined over $FF_p$, where $p approx 2^(254)$ + is the prime from the BN254 specification. +] + +So each point on $E(FF_p)$ is an ordered pair $(X,Y) in FF_p^2$ satisfying @bn254eqn. +Okay, actually, that's a white lie: conventionally, +there is one additional point $O = (0, oo)$ called the "point at infinity" +added in (whose purpose we describe in the next section). + +The constants $p$ and $q$ are contrived so that the following holds: +#theorem[BN254 has prime order][ + Let $E$ be the BN254 curve. + The number of points in $E(FF_p)$, + including the point at infinity $O$, is a prime $q approx 2^(254)$. +] +#definition[ + This prime $q approx 2^(254)$ is affectionately called the *Baby Jubjub prime* + (a reference to #link("https://w.wiki/5Ck3", "The Hunting of the Snark")). + It will usually be denoted by $q$ in these notes. +] + +=== The group law + +So at this point, we have a bag of $q$ points denoted $E(FF_p)$. +However, right now it only has the structure of a set. + +The beauty of elliptic curves +is that it's possible to define an _addition_ operation on the curve; +this is called the #link("https://w.wiki/9jhM", "group law on elliptic curve"). +This addition will make $E(FF_p)$ into an abelian group whose identity element +is $O$. + +This group law involves some kind of heavy algebra. +It's not important to understand exactly how it works. +All you really need to take away from this section is that there is some group law, +and we can program a computer to compute it. + +So, let's get started. +The equation of $E$ is cubic -- the highest-degree terms have degree $3$. +This means that (in general) if you take a line $y = m x + b$ and intersect it with $E$, +the line will meet $E$ in exactly three points. +The basic idea behind the group law is: +If $P, Q, R$ are the three intersection points of a line (any line) +with the curve $E$, then the group-law addition of the three points is +$ +P + Q + R = O. +$ + +(You might be wondering how we can do geometry +when the coordinates $x$ and $y$ are in a finite field. +It turns out that all the geometric operations we're describing -- +like finding the intersection of a curve with a line -- +can be translated into algebra. +And then you just do the algebra in your finite field. +But we'll come back to this.) + +Why three points? +Algebraically, if you take the equations $Y^2 = X^3 + 3$ and $Y = m X + b$ +and try to solve them, +you get +$ +(m X + b)^2 = X^3 + 3, +$ +which is a degree-3 polynomial in $X$, +so it has (at most) 3 roots. +And in fact if it has 2 roots, it's guaranteed to have a third +(because you can factor out the first two roots, and then you're left with a linear factor). + +OK, so given two points $P$ and $Q$, how do we find their sum $P+Q$? +Well, we can draw the line through the two points. +That line -- like any line -- will intersect $E$ in three points: +$P$, $Q$, and a third point $R$. +Now since $P + Q + R = 0$, we know that +$ +- R = P + Q. +$ + +So now the question is just: how to find $-R$? +Well, it turns out that if $R = (x_R, y_R)$, then +$ +- R = (x_R, -y_R). +$ +Why is this? +Well, if you take the vertical line $X = x_R$, +and try to intersect it with the curve, +it looks like there are only two intersection points. +After all, we're solving +$ +Y^2 = x_R^3 + 3, +$ +and since $x_R$ is fixed now, this equation is quadratic. +The two roots are $Y = \pm y_R$. + +OK, there are only two intersection points, but +we say that the third intersection point is "the point at infinity" $O$. +(The reason for this lies in projective geometry, but we won't get into it.) +So the group law here tells us +$ +(x_R, y_R) + (x_R, -y_R) + O = O. +$ +And since $O$ is the identity, we get +$ +-R = (x_R, -y_R). +$ + +So: +- Given a point $P = (x_P, y_P)$, its negative is just $-P = (x, -y)$. +- To add two points $P$ and $Q$, compute the line through the two points, + let $R$ be the third intersection of that line with $E$, + and set + $ + P + Q = -R. + $ + +I just described the group law as a geometric thing, +but there are algebraic formulas to compute it as well. +They are kind of a mess, but here goes. + +If $P = (x_P, y_P)$ and $Q = (x_Q, y_Q)$, then the line between the two points is +$Y = m X + b$, where +$ +m = (y_Q - y_P) / (x_Q - x_P) +$ +and +$ +b = y_P - m x_P. +$ + +The third intersection is $R = (x_R, y_R)$, where +$ +x_R = m^2 - x_P - x_Q +$ +and +$ +y_R = m x_R + b. +$ + +There are separate formulas to deal with various special cases +(if $P = Q$, you need to compute the tangent line to $E$ at $P$, for example), +but we won't get into it. +#todo[Is there a way to... put the above into a box environment or something, so the reader can skip it easily?] + +In summary we have endowed the set of points $E(FF_p)$ with the additional +structure of an abelian group, which happens to have exactly $q$ elements. +However, an abelian group with prime order is necessarily cyclic. +In other words: + +#theorem[The group BN254 is isomorphic to $FF_q$][ + Let $E$ be the BN254 curve. + We have the isomorphism of abelian groups + $ E(FF_p) tilde.equiv ZZ slash q ZZ. $ +] + +In these notes, this isomorphism will basically be a standing assumption; +so moving forward we'll abuse notation slightly +and just write $E$ instead of $E(FF_p)$. + +=== We treat $FF_q$ as the field of scalars henceforth + +Consequently --- and this is important --- +*one should actually think of $FF_q$ as the base field +for all our cryptographic primitives* +(despite the fact that the coordinates of our points are in $FF_p$). + +Whenever we talk about protocols, and there are any sorts of +"numbers" or "scalars" in the protocol, +*these scalars are always going to be elements of $FF_q$*. +Since $q approx 2^(254)$, +that means we are doing something like $256$-bit integer arithmetic. +This is why the baby Jubjub prime $q$ gets a special name, +while the prime $p$ is unnamed and doesn't get any screen-time later. + +== Discrete logarithm is hard + +For our systems to be useful, rather than relying on factoring, +we will rely on the so-called *discrete logarithm* problem. + +#assumption[Discrete logarithm problem][ + Let $E$ be the BN254 curve (or another standardized curve). + Given arbitrary nonzero $g, g' in E$, + it's hard to find an integer $n$ such that $n dot g = g'$. +] + +In other words, if one only +sees $g in E$ and $n dot g in E$, one cannot find $n$. +For cryptography, we generally assume $g$ has order $q$, +so we will talk about $n in NN$ and $n in FF_q$ interchangeably. +In other words, $n$ will generally be thought of as being up to $2^(254)$ in size. + +#remark[The name "discrete log"][ + This problem is called discrete log because if one used multiplicative notation + like in RSA, it looks like solving $g^n = g'$ instead. + We will never use this multiplicative notation in these notes. +] + +On the other hand, given $g in E$, +one can compute $n dot g$ in just $O(log n)$ operations, +by #link("https://w.wiki/9jim", "repeated squaring"). +For example, to compute $400g$, one only needs to do $10$ additions, +rather than $400$: one starts with +$ + 2g &= g + g \ + 4g &= 2g + 2g \ + 8g &= 4g + 4g \ + 16g &= 8g + 8g \ + 32g &= 16g + 16g \ + 64g &= 32g + 32g \ + 128g &= 64g + 64g \ + 256g &= 128g + 128g \ +$ +and then computes +$ 400g = 256g + 128g + 16g. $ + +Because we think of $n$ as up to $q approx 2^(254)$-ish in size, +we consider $O(log n)$ operations like this to be quite tolerable. + +== Curves other than BN254 + +We comment briefly on how the previous two sections adapt to other curves, +although readers could get away with always assuming $E$ is BN254 if they prefer. + +In general, we could have chosen for $E$ any equation of the form +$Y^2 = X^3 + a X + b$ and chosen any prime $p >= 5$ +such that a nondegeneracy constraint $4a^3 + 27b^2 equiv.not 0 mod p$ holds. +In such a situation, $E(FF_p)$ will indeed be an abelian group +once the identity element $O = (0, oo)$ is added in. + +How large is $E(FF_p)$? +There is a theorem called +#link("https://w.wiki/9jhi", "Hasse's theorem") that states +the number of points in $E(FF_p)$ is between $p+1-2sqrt(p)$ and $p+1+2sqrt(p)$. +But there is no promise that $E(FF_p)$ will be _prime_; +consequently, it may not be a cyclic group either. +So among many other considerations, +the choice of constants in BN254 is engineered to get a prime order. + +There are other curves used in practice for which $E(FF_p)$ +is not a prime, but rather a small multiple of a prime. +The popular #link("https://w.wiki/9jhp", "Curve25519") is such a curve +that is also believed to satisfy @ddh. +Curve25519 is defined as $Y^2 = X^3 + 486662X^2 + X$ over $FF_p$ +for the prime $p := 2^(255)-19$. +Its order is actually $8$ times a large prime +$q' := 2^(252) + 27742317777372353535851937790883648493$. +In that case, to generate a random point on Curve25519 with order $q'$, +one will usually take a random point in it and multiply it by $8$. + +BN254 is also engineered to have a property called _pairing-friendly_, +which is defined in @pairing-friendly when we need it later. +(In contrast, Curve25519 does not have this property.) + +== Example application: EdDSA signature scheme + +We'll show how @ddh can be used to construct a signature scheme that replaces RSA. +This scheme is called #link("https://w.wiki/4usy", "EdDSA"), +and it's used quite frequently (e.g. in OpenSSH and GnuPG). +One advantage it has over RSA is that its key size is much smaller: +both the public and private key are 256 bits. +(In contrast, RSA needs 2048-4096 bit keys for comparable security.) + +=== The notation $[n]$ + +Let $E$ be an elliptic curve and let $g in E$ +be a fixed point on it of prime order $q approx 2^(254)$. +For $n in ZZ$ (equivalently $n in FF_q$) we define +$ [n] := n dot g in E. $ + +The hardness of discrete logarithm means that, given $[n]$, we cannot get $n$. +You can almost think of the notation as an "armor" on the integer $n$: +it conceals the integer, but still allows us to perform (armored) addition: +$ [a+b] = [a] + [b]. $ +In other words, $n |-> [n]$ viewed as a map $FF_q -> E$ is $FF_q$-linear. + +=== Signature scheme + +So now suppose Alice wants to set up a signature scheme. + +#algorithm[EdDSA public and secret key][ + 1. Alice picks a random integer $d in FF_q$ as her *secret key*. + 2. Alice publishes $[d] in E$ as her *public key*. +] + +Now suppose Alice wants to prove to Bob that she approves the message $msg$, +given her published public key $[d]$. + +#algorithm[EdDSA signature generation][ + Suppose Alice wants to sign a message $msg$. + + 1. Alice picks a random scalar $r in FF_q$ (keeping this secret) + and publishes $[r] in E$. + 2. Alice generates a number $n in FF_q$ by hashing $msg$ with all public information, + say $ n := sha([r], msg, [d]). $ + 3. Alice publishes the integer $ s := (r + d n) mod q. $ + + In other words, the signature is the ordered pair $([r], s)$. +] + +#algorithm[EdDSA signature generation][ + For Bob to verify a signature $([r], s)$ for $msg$: + + 1. Bob recomputes $n$ (by also performing the hash) and computes $[s] in E$. + 2. Bob verifies that $[r] + n dot [d] = [s]$. +] + +An adversary cannot forge the signature even if they know $r$ and $n$. +Indeed, such an adversary can compute what the point $[s] = [r] + n [d]$ +should be, but without knowledge of $d$ they cannot get the integer $s$, +due to @ddh. + +The number $r$ is called a *blinding factor* because +its use prevents Bob from stealing Alice's secret key $d$ from the published $s$. +It's therefore imperative that $r$ isn't known to Bob +nor reused between signatures, and so on. +One way to do this would be to pick $r = sha(d, msg)$; this has the +bonus that it's deterministic as a function of the message and signer. + +In @kzg we will use ideas quite similar to this to +build the KZG commitment scheme. + +== Example application: Pedersen commitments + +A multivariable corollary of @ddh is that if $g_1, ..., g_n in E$ +are a bunch of randomly chosen points of $E$ with order $q$, +then it's computationally infeasible to find +$(a_1, ..., a_n) != (b_1, ..., b_n) in FF_q^n$ such that +$ a_1 g_1 + ... + a_n g_n = b_1 g_1 + ... + b_n g_n. $ +Indeed, even if one fixes any choice of $2n-1$ of the $2n$ coefficients above, +one cannot find the last coefficient. + +#definition[ + In these notes, if there's a globally known elliptic curve $E$ + and points $g_1, ..., g_n$ have order $q$ and no known nontrivial + linear dependencies between them, + we'll say they're a *computational basis over $FF_q$*. +] + +#remark[ + This may horrify pure mathematicians because we're pretending the map + $ FF_q^n -> FF_q " by " (a_1, ..., a_n) |-> sum_1^n a_i g_i $ + is injective, + even though the domain is an $n$-dimensional $FF_q$-vector space + and the codomain is one-dimensional. + This can feel weird because our instincts from linear algebra in pure math + are wrong now. This map, while not injective in theory, + ends up being injective _in practice_ (because we can't find collisions). + And this is a critical standing assumption for this entire framework! +] + +This injectivity gives us a sort of hash function on vectors +(with "linearly independent" now being phrased as "we can't find a collision"). +To spell this out: + +#definition[ + Let $g_1, ..., g_n in E$ be a computational basis over $FF_q$. + Given a vector + $ arrow(a) = angle.l a_1, ..., a_n angle.r in FF_q^n $ of scalars, + the group element + $ sum a_i g_i = a_1 g_1 + ... + a_n g_n in E $ + is called the *Pedersen commitment* of our vector $arrow(a)$. +] + +The Pedersen commitment is thus a sort of hash function: +given the group element above, +one cannot recover any of the $a_i$ (even when $n=1$); +but given the entire vector $arrow(a)$ +one can compute the Pedersen commitment easily. + +We won't used Pedersen commitments in this book, +but in @kzg we will see a closely related commitment scheme, +called KZG. + diff --git a/easy/src/kzg.typ b/easy/src/kzg.typ new file mode 100644 index 0000000..24ede9f --- /dev/null +++ b/easy/src/kzg.typ @@ -0,0 +1,133 @@ +#import "preamble.typ":* + += Kate-Zaverucha-Goldberg (KZG) commitments + +== Pitch: KZG lets you commit a polynomial and reveal individual values + +The goal of the KZG commitment schemes is to have the following API: + +- Peggy has a secret polynomial $P(X) in FF_q [X]$. +- Peggy sends a short "commitment" to the polynomial (like a hash). +- This commitment should have the additional property that + Peggy should be able to "open" the commitment at any $z in FF_q$. + Specifically: + + - Victor has an input $z in FF_q$ and wants to know $P(z)$. + - Peggy knows $P$ so she can compute $P(z)$; + she sends the resulting number $y = P(z)$ to Victor. + - Peggy can then send a short "proof" convincing Victor that $y$ is the + correct value, without having to reveal $P$. + +The KZG commitment scheme is amazingly efficient because both the commitment +and proof lengths are a single point on $E$, encodable in 256 bits. + +== Elliptic curve setup done once + +The good news is that this can be done just once, period. +After that, anyone in the world can use the published data to run this protocol. + +For concreteness, $E$ will be the BN256 curve and $g$ a fixed generator. + +=== The notation $[n]$ + +We retain the notation $[n] := n dot g in E$ defined in @armor. + +=== Trusted calculation + +To set up the KZG commitment scheme, +a trusted party needs to pick a secret scalar $s in FF_q$ and publishes +$ [s^0], [s^1], ..., [s^M] $ +for some large $M$, the maximum degree of a polynomial the scheme needs to support. +This means anyone can evaluate $[P(s)]$ for any given polynomial $P$ of degree up to $M$. +(For example, $[s^2+8s+6] = [s^2] + 8[s] + 6[1]$.) +Meanwhile, the secret scalar $s$ is never revealed to anyone. + +This only needs to be done by a trusted party once for the curve $E$. +Then anyone in the world can use the resulting sequence for KZG commitments. + +#remark[ + The trusted party has to delete $s$ after the calculation. + If anybody knows the value of $s$, the protocol will be insecure. + The trusted party will only publish $[s^0] = [1], [s^1], ..., [s^M]$. + Given these published values, it is (probably) extremely hard to recover $s$ -- + this is a case of the discrete logarithm problem. + + You can make the protocol somewhat more secure by involving several different trusted parties. + The first party chooses a random $s_1$, computes $[s_1^0], ..., [s_1^M]$, and then discards s_1. + The second party chooses $s_2$ and computes + $[(s_1 s_2)^0], ..., [(s_1 s_2)^M]$. + And so forth. + In the end, the value $s$ will be the product of the secrets $s_i$ + chosen by the $i$ parties... so the only way they can break secrecy + is if all the "trusted parties" collaborate. +] + +== The KZG commitment scheme + +Peggy has a polynomial $P(X) in FF_p [X]$. +To commit to it: + +#algorithm("Creating a KZG commitment")[ + 1. Peggy computes and publishes $[P(s)]$. +] +This computation is possible as $[s^i]$ are globally known. + +Now consider an input $z in FF_p$; Victor wants to know the value of $P(z)$. +If Peggy wishes to convince Victor that $P(z) = y$, then: + +#algorithm("Opening a KZG commitment")[ + 1. Peggy does polynomial division to compute $Q(X) in FF_q [X]$ such that + $ P(X)-y = (X-z) Q(X). $ + 2. Peggy computes and sends Victor $[Q(s)]$, + which again she can compute from the globally known $[s^i]$. + 3. Victor verifies by checking + #eqn[ + $ pair([Q(s)], [s]-[z]) = pair([P(s)]-[y], [1]) $ + + ] + and accepts if and only if @kzg-verify is true. +] + +If Peggy is truthful, then @kzg-verify will certainly check out. + +If $y != P(z)$, then Peggy can't do the polynomial long division described above. +So to cheat Victor, she needs to otherwise find an element +$ 1/(s-x) ([P(s)]-[y]) in E. $ +Since $s$ is a secret nobody knows, there isn't any known way to do this. + +== Multi-openings + +To reveal $P$ at a single value $z$, we did polynomial division +to divide $P(X)$ by $X-z$. +But there's no reason we have to restrict ourselves to linear polynomials; +this would work equally well with higher-degree polynomials, +while still using only a single 256-bit for the proof. + +For example, suppose Peggy wanted to prove that +$P(1) = 100$, $P(2) = 400$, ..., $P(9) = 8100$. +Then she could do polynomial long division to get a polynomial $Q$ +of degree $deg(P) - 9$ such that +$ P(X) - 100X^2 = (T-1)(T-2) ... (T-9) dot Q(T). $ +Then Peggy sends $[Q(s)]$ as her proof, and the verification equation is that +$ pair([Q(s)], [(s-1)(s-2) ... (s-9)]) = pair([P(s)] - 100[s^2], [1]). $ + +The full generality just replaces the $100T^2$ with the polynomial +obtained from #link("https://w.wiki/8Yin", "Lagrange interpolation") +(there is a unique such polynomial $f$ of degree $n-1$). +To spell this out, suppose Peggy wishes to prove to Victor that +$P(z_i) = y_i$ for $1 <= i <= n$. + +#algorithm[Opening a KZG commitment at $n$ values][ + 1. By Lagrange interpolation, both parties agree on a polynomial $f(X)$ + such that $f(z_i) = y_i$. + 2. Peggy does polynomial long division to get $Q(X)$ such that + $ P(X) - f(X) = (X-z_1)(X-z_2) ... (X-z_n) dot Q(X). $ + 3. Peggy sends the single element $[Q(s)]$ as her proof. + 4. Victor verifies + $ pair([Q(s)], [(s-z_1)(s-z_2) ... (s-z_n)]) = pair([P(s)] - [f(s)], [1]). $ +] + +So one can even open the polynomial $P$ at $1000$ points with a single 256-bit proof. +The verification runtime is a single pairing plus however long +it takes to compute the Lagrange interpolation $f$. + diff --git a/easy/src/mpc.md b/easy/src/mpc.md new file mode 100644 index 0000000..328002b --- /dev/null +++ b/easy/src/mpc.md @@ -0,0 +1,180 @@ +--- +title: "Motivating Garbled Circuits" +permalink: "/motivating-garbled-circuits" +date: April 17, 2024 +postType: 0 +--- + +# Motivating Garbled Circuits + +Cryptographic protocols can sometimes feel a bit like magic. If you're like us, you've probably had the experience where you can verify that a protocol is probably complete (and if you're lucky, you might even be convinced that it's sound), but you might have no idea as to _why_ the protocol works the way it does, or how one might even have arrived at the construction in the first place. So attempting to rederive a protocol is often helpful for understanding the key ideas or "cruxes." + +In this post, we'll explain how one might rederive Yao's Garbled Circuits protocol from scratch, from an intuitive perspective. Garbled Circuits are a neat primitive for general-purpose 2-party computation, but the construction may seem a bit "magical"--how would you come up with the idea of garbling, in a world where it doesn't exist? + +Garbled Circuits are a solution to the general problem of 2-party computation, or 2PC. In 2PC, Alice and Bob each have some secret values $a$ and $b$, and would like to jointly compute some function $f$ over their respective inputs. Furthermore, they'd like to keep their secret values hidden from each other: if Alice and Bob follow the protocol honestly, they should both end up learning the correct value of $f(a,b)$, but Alice shouldn't learn anything about $b$ (other than what could be learned by knowing both $a$ and $f(a,b)$), and likewise for Bob. + +Yao's Garbled Circuits is one of the most well-known 2PC protocols (Vitalik has a great explanation [here](https://vitalik.eth.limo/general/2020/03/21/garbled.html)). The protocol is quite clever, and optimized variants of the protocol are being [implemented and used today](https://github.com/privacy-scaling-explorations/mpz/tree/dev/garble). + +We don't know exactly what led Yao to the idea of garbling, but we'll describe one plausible path for someone today to independently rediscover garbling, assuming the existence of an earlier primitive called "Oblivious Transfer." + +## The Problem + +Here is our problem setting, slightly more formally: + +- $A$ knows a secret bitstring $a$ of length $s$ bits +- $B$ knows a secret bitstring $b$ of length $t$ bits +- $C$ is a binary circuit, which takes in $s + t$ bits, and runs them through some $n$ gates. The outputs of some of the gates are the public outputs of the circuit. Without loss of generality, let's also suppose that each gate in $C$ accepts either $1$ or $2$ input bits, and outputs a single output bit. +- $A$ and $B$ would like to jointly compute $C(a, b)$ without revealing to each other their secrets. + +## Precursor: Oblivious Transfer + +If you're aware of Oblivious Transfer (OT), an earlier cryptoraphic primitive, it's plausibly one of the first things you might try to use if you're approaching the general problem of 2PC. + +As a quick primer: Oblivious Transfer protocols also involve two parties, Alice and Bob. Alice has a tuple of some records, $(x_1, x_2, ..., x_n)$ (imagine that each of these values is some 32-bit integer). Bob would like to query for the value $x_i$, for some specific index $i$, without letting Alice know which $i$ he is asking for. Alice would like for Bob to only learn the single value of $x_i$, without learning anything about $x_j$ for $j \neq i$. + +This might seem almost paradoxical at first glance. However, with a bit of cleverness, you can design a protocol such that Alice and Bob can carry this procedure out by transmitting $cn$ bits between each other, where $c$ is in the hundreds. Today, protocols where Alice and Bob communicate sublinear information in $n$ exist as well! + +- If you're curious, we break down two simple Oblivious Transfer protocols in [another post](https://hackmd.io/4BcDxaUdS4yDkB9B0HzMow). + +If you meditate on OT and 2PC for a bit, you might see how the "API" of Oblivious Transfer is nearly identical the "API" of 2PC. (We'll explain exactly what that correspondence looks like in the next section.) + +## OTing a Huge Function Lookup Table is 2PC + +In the previous section, we mentioned that OT and 2PC have the same "API." This comes from the observation that a function can be seen as a big lookup table, so reading from a lookup table is somehow equivalent to evaluating a function. + +The most straightforward thing Alice can do is to simply construct a huge lookup table of all the possible values of $C(a,b)$, given her fixed value of $a$, and then allow Bob to query for the appropriate value of $C(a,b)$ for his $b$. + +For a concrete example, suppose $t = \operatorname{len}(b) = 3$. Knowing $a$, Alice is able to calculate the following lookup table: + +| $b_1, b_2, b_3$ | $C(a, b)$ | +| --------------- | ----------- | +| 000 | $C(a, 000)$ | +| 001 | $C(a, 001)$ | +| 010 | $C(a, 010)$ | +| 011 | $C(a, 011)$ | +| 100 | $C(a, 100)$ | +| 101 | $C(a, 101)$ | +| 110 | $C(a, 110)$ | +| 111 | $C(a, 111)$ | + +Then she allows Bob to retrieve the value corresponding to $b$, using Oblivious Transfer. Bob learns the correct value of $C(a, b)$ and shares this with Alice. + +While this protocol works, it's clearly not very efficient. It requires Alice to construct and perform OT with a lookup table of size $2^t$, which grows exponentially larger in the length of Bob's input. How can we improve this? + +## Can we perform OT Gate-By-Gate? + +$C$ has lots of structure: it can be broken down into a bunch of gates. A natural question to ask is whether we can somehow perform our OT-lookup idea gate-by-gate, instead of on the whole circuit. Since gates are small, our lookup tables would be much smaller. + +Let's start with a single AND gate: $x_1 \wedge x_2 = x_3$, where Alice knows $x_1$, and Bob knows $x_2$. The output of this gate is some intermediate result $x_3$ will get fed into some other gates deeper in the circuit. + +What happens when we try to blindly apply our lookup OT procedure in computing $x_3$? According to our procedure, Alice constructs a lookup table that specifies the result of $x_3$ for the possible values of $x_2$, and Bob will retrieve the value of $x_3$ corresponding to his value of $x_2$. Finally, Bob tells Alice the value $x_3$. + +With this protocol, if Alice and Bob's bits are both $1$, they'll learn this. But if either of their bits is $0$, the person with a $0$ bit will learn nothing about the other person's bit. + +Can we build a multi-gate 2PC protocol off of this directly? Well, we'll run into two problems: + +- Bob (and Alice) learn the value of $x_3$ at the end of the procedure. But in a larger circuit, $x_3$ is an intermediate value, and we only want Bob and Alice to learn the final values--they shouldn't learn anything about the intermediate values of the circuit! +- As we mentioned, Bob might also learn something about Alice's value of $x_1$. For example, if $x_3$ is $1$ and $x_2$ is $1$, Bob knows that $x_1$ is $1$ as well. + +So we can't blindly apply our lookup OT gate-by-gate, but with a bit of tweaking we can maybe still construct a modified lookup-OT-procedure that can be "chained," gate by gate. + +## Computing a Gate on Secret Shares + +We'd like for Alice and Bob to each learn some information that would allow them to _jointly_ compute the output of the gate, but we don't want either of them to actually be able to know learn the result on their own without consulting the other. A common tactic for this kind of setting is to have Alice and Bob try to compute "secret shares" of the result. + +What this means is that Alice and Bob might try to design a procedure where Alice will end up with some $a_3$ and Bob will end up with some $b_3$ such that $a_3 \oplus b_3 = x_3$, where $x_3$ is the expected output of the gate. If Alice's $a_3$ is drawn from a random distribution, then neither Alice nor Bob have gained any information on $x_3$, but they've managed to "jointly compute" the gate. + +Even if Alice and Bob can do this, there's still some trickiness: in future computations involving $x_3$, Alice and Bob will have to both bring their respective shares, and we'll have to figure out how to pass secret shares through gates. + +But if we can figure out how to deal with that, a possible strategy emerges: + +- Alice and Bob proceed through the computation of $C$ gate-by-gate. For each gate, they plug in the secret shares into the gate's inputs (which they've previously computed), and then produce secret shares of the gate's output. These output secret shares can be used in future gates. + - To reiterate: by "secret shares", we mean - if the output of a gate is $x_i$, Alice should learn some $a_i$ and Bob should learn some $b_i$, such that $a_i \oplus b_i = x_i$. + - All shares for intermediate values should be randomized, so that neither $A$ nor $B$ should learn anything about the intermediate bits or about each other's bits, though taken as a collective $A$ and $B$ are tracking the computation correctly as it progresses. +- At the end of the computation, $A$ and $B$ will reveal their secret shares of the output bits, and combine them together to retrieve the output. + +It turns out that it's a pretty straightforward extension to modify our lookup-OT tactic to make it work for computing gates on secret shares. (We recommend trying to figure this out yourself before reading the next paragraph!) + +Suppose that $G_i$ is gate with two inputs, and that $G_i$ computes $x_i = x_j \diamondsuit x_k$ for some operator $\diamondsuit$. Alice knows two bits $a_j, a_k$, and Bob knows two bits $b_j, b_k$, such that $a_j \oplus b_j = x_j$ and $a_k \oplus b_k = x_k$. Alice would like to end up computing some $a_i$ and Bob some $b_i$ such that $a_i \oplus b_i = x_i$ is indeed the correct result of the gate. + +First, Alice flips a coin to choose a random bit for $a_i$. Then, she computes a lookup table that describes what $b_i$ should be, conditional on $b_j, b_k$, and given $A$'s fixed values of $a_i, a_j, a_k$: + +| $b_j, b_k$ | $b_i$ | +| ---------- | --------------------------------------------------- | +| 00 | $b_i = (a_j \diamondsuit a_k) \oplus a_i$ | +| 01 | $b_i = (a_j \diamondsuit \neg a_k) \oplus a_i$ | +| 10 | $b_i = (\neg a_j \diamondsuit a_k) \oplus a_i$ | +| 11 | $b_i = (\neg a_j \diamondsuit \neg a_k) \oplus a_i$ | + +How do we fill in the values in the $b_i$ column? Well, we know that $(a_j \oplus b_j) \diamondsuit (a_k \oplus b_k) = a_i \oplus b_i$; that's what it means to compute the gate $G_i$ over secret shares. Since Alice knows $a_j, a_k, a_i$, for each row she can substitute in values of $b_j, b_k$ suggested by the row label, and then solve for $b_i$. + +To complete the computation, Alice and Bob can simply carry out an oblivious transfer. Bob knows his values $b_j$ and $b_k$, so he should be able to ask for the appropriate row of the table containing the correct value of $b_i$ without Alice learning which row he is asking for. The two of them have now jointly computed a gate, in a "chainable" way, without learning anything about the gate outputs! + +## A full gate-by-gate protocol + +Once we've built a gadget for computing secret-share-outputs of a gate from secret-share-inputs, we can chain these together into a complete protocol. For completeness, we've written down the full protocol below. + +- First, topologically sort all of the gates $G_1, G_2, ..., G_n$. We'll denote the output of gate $G_i$ as $x_i$, and denote Alice and Bob's shares of $x_i$ as $a_i$ and $b_i$ respectively. Our goal is for Alice and Bob to jointly compute all of their $a_i$ and $b_i$ shares, going gate-by-gate. + - We'll also handle the inputs as follows: first, for the purposes of this algorithm, we will consider the input bits to be gates as well. $A$'s input bits will be the first $s$ "gates", and $B$'s input bits will be the next $t$ "gates." For the first $s$ gates, $a_i = x_i$ and $b_i = 0$. For the next $t$ gates, $b_i = x_i$ and $a_i = 0$. +- Now, we go gate-by-gate, starting at gate $G_{s+t+1}$, and use a lookup-OT procedure to enable Alice to compute $a_i$ and Bob to compute $b_i$ as $i$ increments. There are two cases for any given gate: + - If $G_i$ is a gate with a single input, then it's a NOT gate. In this case, $x_i = \neg x_j$ for some $j < i$. If $G_i$ is a gate of this form, $A$ sets $a_i = \neg a_j$ and $B$ sets $b_i = b_j$. + - In the second case, Alice and Bob use the secret-share-gate OT protocol described above. + +With this protocol, $A$ and $B$ can compute all values of $a_i$ and $b_i$. At the end they simply reveal and xor their desired output bits together! + +Note that we can reduce the number of OTs with a few optimizations. For example, any time $\diamondsuit$ is a linear operator (such as xor), $A$ and $B$ can simply apply that linear operator to their respective shares. So we only need to perform OTs for the nonlinear gates. + +## Yao's Garbled Circuits: Replacing Per-Gate OTs With Encryption + +Though it might not seem like it on the surface, we've now arrived at something that is actually quite close to [Yao's garbled circuits protocol](https://vitalik.eth.limo/general/2020/03/21/garbled.html). In fact, Yao's protocol can be seen as the above protocol with some optimizations, that enable us to replace a bunch of OTs with "precomputation" from Alice. + +OTs are expensive, and doing one OT per gate is still pretty inefficient (though it's certainly far better than an OT of size $2^s$!). Each OT also requires a round of interaction between Alice and Bob, meaning that this protocol involves many, many back-and-forths between Alice and Bob (not ideal). + +A natural question to ask would be: is there a way that we can somehow "batch" all of the OTs, or else perform some kind of precomputation so that Alice and Bob can learn their secret shares at each gate non-interactively? + +### Observation: Alice never actually interacts with Bob + +This question becomes especially interesting in light of the following observation: in our previous protocol, it turns out that Alice's $a_i$ values are selected completely independently of anything Bob does. In other words, at the start of the protocol Alice could have simply decided to write down all of her $a_i$'s in advance. + +In this view, Alice is essentially writing down a "scrambling" function for the circuit trace at the very start of the protocol. Concretely, the tuple $\{b_1, ..., b_n\}$ that Bob computes over the course of the protocol will end up being a scrambled version of the "true" trace $\{x_1, ..., x_n\}$, where the scrambling is decided by Alice at the beginning of the protocol. + +What does it mean to see Alice's "scrambling" as a function, or set of functions? Well, Alice is essentially defining a scrambling map $g_i$ for each gate (including the input "gates"). For each gate $G_i$, the scrambling map $g_i$ takes $\{0, 1\}$ to some random shuffling of $\{0, 1\}$. Specifically: + +- If $a_i = 0$, then $g_i(0) = 0$ and $g_i(1) = 1$ +- If $a_i = 1$, then $g_i(0) = 1$ and $g_i(1) = 0$ + +So, to sum up this alternative view of our latest protocol: Alice writes down a bunch of scrambling functions $g_i$ at the start of the protocol, and then over the course of the protocol Bob gradually learns the scrambled version of the true circuit trace $x_i$. In particular, he's learning $b_i = g_i(x_i)$ for all $i$. + +### Replacing OTs with Encryption + +Currently, Bob learns his "scrambled" values through OT. If we want to find a way to remove the requirement of "one OT per gate," we need to figure out a way for Bob to learn the scrambled trace non-interactively. + +This seems plausible. Alice shouldn't really need to interact with Bob at all, until the very end of the protocol. She wrote down the scrambling at the beginning of the protocol, and she's not supposed to learn anything about Bob's values or what he's doing anyway; so it seems like it might be possible for Alice to simply send Bob some information that allows him to figure out his $b_i$'s without too much more interaction with her. + +Let's try to do this in the most direct way. For a gate $G_i$ that represents the intermediate calculation $x_i = x_j \diamondsuit x_k$, Alice might publish a table that looks something like the following for Bob: + +| $b_j = g_j(x_j), b_k = g_k(x_k)$ | $b_i = g_i(x_i)$ | +| -------------------------------- | ------------------------------------------------------------------- | +| 00 | $\operatorname{Enc}_1((a_j \diamondsuit a_k) \oplus a_i$) | +| 01 | $\operatorname{Enc}_2((a_j \diamondsuit \neg a_k) \oplus a_i)$ | +| 10 | $\operatorname{Enc}_3((\neg a_j \diamondsuit a_k) \oplus a_i)$ | +| 11 | $\operatorname{Enc}_4((\neg a_j \diamondsuit \neg a_k) \oplus a_i)$ | + +(This table is the same as the lookup table from above, just with the $b$'s written as outputs of scrambling functions, and with the values in the second column "locked" by encryption). + +This table should be constructed so that Bob can only "unlock" the appropriate value of $g_i(x_i)$ for the row of $g_j(x_j), g_k(x_k)$ values that he actually possesses--this simulates the OT property that Bob can't learn the values of any other rows. For example, $\operatorname{Enc}_1$ should only be possible for Bob to decrypt if $g_j(x_j) = 0$ and $g_k(x_k) = 0$. + +But it turns out that there's a very direct way to do this! We simply have our scrambling $g_j$ and $g_k$ _also_ output some symmetric encryption keys picked by Alice at the start of the protocol, in addition to the scrambled bit. The values in the second column should be encrypted with both of these keys, and the output of $g_i$ should also include a new symmetric encryption key as well that can be used to unlock later values. + +For example, suppose that for some value $x_j$ we have $g_j(x_j) = (1, \mathsf{key}_{j1})$, and for some value of $x_k$ we have $g_k(x_k) = (0, \mathsf{key}_{k0})$. Then $\operatorname{Enc}_3$ is symmetric encryption with a key derived from $\mathsf{key}_{j1}$ and $\mathsf{key}_{k0}$ - for example, $\operatorname{Hash}(\mathsf{key}_{j1}, \mathsf{key}_{k0})$. + +In other words, the output of our "scrambling" function $g_i$ should actually be a _tuple_ of two values: a scrambled bit, and a symmetric encryption key that can be used to decrypt values in lookup tables deeper in the circuit, where outputs of $g_i$ are involved: + +$g_i(0) = (a_i, \mathsf{key}_{ia_i})$ +$g_i(1) = (\neg a_i, \mathsf{key}_{i (\neg a_i)})$ + +Now, for each $i$, Bob learns both the scrambled bit of $g_i(x_i)$, as well as a symmetric encryption key that can be used to decrypt future outputs for which $x_i$ is an input. At the very end, Bob asks Alice to reveal the scrambling functions for the final circuit outputs, and then unscrambles his output bits. + +It turns out that there are still a few more kinks to iron out, like handling inputs--for example, we'll find that our lazy default of setting $a_i = x_i, b_i = 0$ for Alice's inputs and $b_i = x_i, a_i = 0$ for Bob's inputs won't work anymore. But we've essentially arrived at what we now know today as Yao's Garbled Circuits protocol! In the Garbled Circuits protocol, our operation of "scrambling" is instead called "garbling." + +**Final exercise for the reader**: Work out the remaining kinks to arrive at a complete Garbled Circuits protocol. \ No newline at end of file diff --git a/easy/src/mpc.typ b/easy/src/mpc.typ new file mode 100644 index 0000000..c89d63a --- /dev/null +++ b/easy/src/mpc.typ @@ -0,0 +1,409 @@ += Motivating Garbled Circuits + +Cryptographic protocols can sometimes feel a bit like magic. If you’re +like us, you’ve probably had the experience where you can verify that a +protocol is probably complete (and if you’re lucky, you might even be +convinced that it’s sound), but you might have no idea as to #emph[why] +the protocol works the way it does, or how one might even have arrived +at the construction in the first place. So attempting to rederive a +protocol is often helpful for understanding the key ideas or "cruxes." + +In this post, we’ll explain how one might rederive Yao’s Garbled +Circuits protocol from scratch, from an intuitive perspective. Garbled +Circuits are a neat primitive for general-purpose 2-party computation, +but the construction may seem a bit "magical"–how would you come up with +the idea of garbling, in a world where it doesn’t exist? + +Garbled Circuits are a solution to the general problem of 2-party +computation, or 2PC. In 2PC, Alice and Bob each have some secret values +$a$ and $b$, and would like to jointly compute some function $f$ over +their respective inputs. Furthermore, they’d like to keep their secret +values hidden from each other: if Alice and Bob follow the protocol +honestly, they should both end up learning the correct value of +$f (a , b)$, but Alice shouldn’t learn anything about $b$ (other than +what could be learned by knowing both $a$ and $f (a , b)$), and likewise +for Bob. + +Yao’s Garbled Circuits is one of the most well-known 2PC protocols +(Vitalik has a great explanation +#link("https://vitalik.eth.limo/general/2020/03/21/garbled.html")[here];). +The protocol is quite clever, and optimized variants of the protocol are +being +#link("https://github.com/privacy-scaling-explorations/mpz/tree/dev/garble")[implemented and used today];. + +We don’t know exactly what led Yao to the idea of garbling, but we’ll +describe one plausible path for someone today to independently +rediscover garbling, assuming the existence of an earlier primitive +called "Oblivious Transfer." + +== The Problem + +Here is our problem setting, slightly more formally: + +- $A$ knows a secret bitstring $a$ of length $s$ bits +- $B$ knows a secret bitstring $b$ of length $t$ bits +- $C$ is a binary circuit, which takes in $s + t$ bits, and runs them + through some $n$ gates. The outputs of some of the gates are the + public outputs of the circuit. Without loss of generality, let’s also + suppose that each gate in $C$ accepts either $1$ or $2$ input bits, + and outputs a single output bit. +- $A$ and $B$ would like to jointly compute $C (a , b)$ without + revealing to each other their secrets. + +== Precursor: Oblivious Transfer + +If you’re aware of Oblivious Transfer (OT), an earlier cryptoraphic +primitive, it’s plausibly one of the first things you might try to use +if you’re approaching the general problem of 2PC. + +As a quick primer: Oblivious Transfer protocols also involve two +parties, Alice and Bob. Alice has a tuple of some records, +$(x_1 , x_2 , . . . , x_n)$ (imagine that each of these values is some +32-bit integer). Bob would like to query for the value $x_i$, for some +specific index $i$, without letting Alice know which $i$ he is asking +for. Alice would like for Bob to only learn the single value of $x_i$, +without learning anything about $x_j$ for $j eq.not i$. + +This might seem almost paradoxical at first glance. However, with a bit +of cleverness, you can design a protocol such that Alice and Bob can +carry this procedure out by transmitting $c n$ bits between each other, +where $c$ is in the hundreds. Today, protocols where Alice and Bob +communicate sublinear information in $n$ exist as well! + +- If you’re curious, we break down two simple Oblivious Transfer + protocols in + #link("https://hackmd.io/4BcDxaUdS4yDkB9B0HzMow")[another post];. + +If you meditate on OT and 2PC for a bit, you might see how the "API" of +Oblivious Transfer is nearly identical the "API" of 2PC. (We’ll explain +exactly what that correspondence looks like in the next section.) + +== OTing a Huge Function Lookup Table is 2PC + +In the previous section, we mentioned that OT and 2PC have the same +"API." This comes from the observation that a function can be seen as a +big lookup table, so reading from a lookup table is somehow equivalent +to evaluating a function. + +The most straightforward thing Alice can do is to simply construct a +huge lookup table of all the possible values of $C (a , b)$, given her +fixed value of $a$, and then allow Bob to query for the appropriate +value of $C (a , b)$ for his $b$. + +For a concrete example, suppose $t = "len" (b) = 3$. Knowing $a$, Alice +is able to calculate the following lookup table: + +#figure( + align(center)[#table( + columns: 2, + align: (auto,auto,), + table.header([$b_1 , b_2 , b_3$], [$C (a , b)$],), + table.hline(), + [000], [$C (a , 000)$], + [001], [$C (a , 001)$], + [010], [$C (a , 010)$], + [011], [$C (a , 011)$], + [100], [$C (a , 100)$], + [101], [$C (a , 101)$], + [110], [$C (a , 110)$], + [111], [$C (a , 111)$], + )] + , kind: table + ) + +Then she allows Bob to retrieve the value corresponding to $b$, using +Oblivious Transfer. Bob learns the correct value of $C (a , b)$ and +shares this with Alice. + +While this protocol works, it’s clearly not very efficient. It requires +Alice to construct and perform OT with a lookup table of size $2^t$, +which grows exponentially larger in the length of Bob’s input. How can +we improve this? + +== Can we perform OT Gate-By-Gate? + +$C$ has lots of structure: it can be broken down into a bunch of gates. +A natural question to ask is whether we can somehow perform our +OT-lookup idea gate-by-gate, instead of on the whole circuit. Since +gates are small, our lookup tables would be much smaller. + +Let’s start with a single AND gate: $x_1 and x_2 = x_3$, where Alice +knows $x_1$, and Bob knows $x_2$. The output of this gate is some +intermediate result $x_3$ will get fed into some other gates deeper in +the circuit. + +What happens when we try to blindly apply our lookup OT procedure in +computing $x_3$? According to our procedure, Alice constructs a lookup +table that specifies the result of $x_3$ for the possible values of +$x_2$, and Bob will retrieve the value of $x_3$ corresponding to his +value of $x_2$. Finally, Bob tells Alice the value $x_3$. + +With this protocol, if Alice and Bob’s bits are both $1$, they’ll learn +this. But if either of their bits is $0$, the person with a $0$ bit will +learn nothing about the other person’s bit. + +Can we build a multi-gate 2PC protocol off of this directly? Well, we’ll +run into two problems: + +- Bob (and Alice) learn the value of $x_3$ at the end of the procedure. + But in a larger circuit, $x_3$ is an intermediate value, and we only + want Bob and Alice to learn the final values–they shouldn’t learn + anything about the intermediate values of the circuit! +- As we mentioned, Bob might also learn something about Alice’s value of + $x_1$. For example, if $x_3$ is $1$ and $x_2$ is $1$, Bob knows that + $x_1$ is $1$ as well. + +So we can’t blindly apply our lookup OT gate-by-gate, but with a bit of +tweaking we can maybe still construct a modified lookup-OT-procedure +that can be "chained," gate by gate. + +== Computing a Gate on Secret Shares + +We’d like for Alice and Bob to each learn some information that would +allow them to #emph[jointly] compute the output of the gate, but we +don’t want either of them to actually be able to know learn the result +on their own without consulting the other. A common tactic for this kind +of setting is to have Alice and Bob try to compute "secret shares" of +the result. + +What this means is that Alice and Bob might try to design a procedure +where Alice will end up with some $a_3$ and Bob will end up with some +$b_3$ such that $a_3 xor b_3 = x_3$, where $x_3$ is the expected output +of the gate. If Alice’s $a_3$ is drawn from a random distribution, then +neither Alice nor Bob have gained any information on $x_3$, but they’ve +managed to "jointly compute" the gate. + +Even if Alice and Bob can do this, there’s still some trickiness: in +future computations involving $x_3$, Alice and Bob will have to both +bring their respective shares, and we’ll have to figure out how to pass +secret shares through gates. + +But if we can figure out how to deal with that, a possible strategy +emerges: + +- Alice and Bob proceed through the computation of $C$ gate-by-gate. For + each gate, they plug in the secret shares into the gate’s inputs + (which they’ve previously computed), and then produce secret shares of + the gate’s output. These output secret shares can be used in future + gates. + - To reiterate: by "secret shares", we mean - if the output of a gate + is $x_i$, Alice should learn some $a_i$ and Bob should learn some + $b_i$, such that $a_i xor b_i = x_i$. + - All shares for intermediate values should be randomized, so that + neither $A$ nor $B$ should learn anything about the intermediate + bits or about each other’s bits, though taken as a collective $A$ + and $B$ are tracking the computation correctly as it progresses. +- At the end of the computation, $A$ and $B$ will reveal their secret + shares of the output bits, and combine them together to retrieve the + output. + +It turns out that it’s a pretty straightforward extension to modify our +lookup-OT tactic to make it work for computing gates on secret shares. +(We recommend trying to figure this out yourself before reading the next +paragraph!) + +Suppose that $G_i$ is gate with two inputs, and that $G_i$ computes +$x_i = x_j ♢ x_k$ for some operator $♢$. Alice knows two bits +$a_j , a_k$, and Bob knows two bits $b_j , b_k$, such that +$a_j xor b_j = x_j$ and $a_k xor b_k = x_k$. Alice would like to end up +computing some $a_i$ and Bob some $b_i$ such that $a_i xor b_i = x_i$ is +indeed the correct result of the gate. + +First, Alice flips a coin to choose a random bit for $a_i$. Then, she +computes a lookup table that describes what $b_i$ should be, conditional +on $b_j , b_k$, and given $A$’s fixed values of $a_i , a_j , a_k$: + +#figure( + align(center)[#table( + columns: 2, + align: (auto,auto,), + table.header([$b_j , b_k$], [$b_i$],), + table.hline(), + [00], [$b_i = (a_j ♢ a_k) xor a_i$], + [01], [$b_i = (a_j ♢ not a_k) xor a_i$], + [10], [$b_i = (not a_j ♢ a_k) xor a_i$], + [11], [$b_i = (not a_j ♢ not a_k) xor a_i$], + )] + , kind: table + ) + +How do we fill in the values in the $b_i$ column? Well, we know that +$(a_j xor b_j) ♢ (a_k xor b_k) = a_i xor b_i$; that’s what it means to +compute the gate $G_i$ over secret shares. Since Alice knows +$a_j , a_k , a_i$, for each row she can substitute in values of +$b_j , b_k$ suggested by the row label, and then solve for $b_i$. + +To complete the computation, Alice and Bob can simply carry out an +oblivious transfer. Bob knows his values $b_j$ and $b_k$, so he should +be able to ask for the appropriate row of the table containing the +correct value of $b_i$ without Alice learning which row he is asking +for. The two of them have now jointly computed a gate, in a "chainable" +way, without learning anything about the gate outputs! + +== A full gate-by-gate protocol + +Once we’ve built a gadget for computing secret-share-outputs of a gate +from secret-share-inputs, we can chain these together into a complete +protocol. For completeness, we’ve written down the full protocol below. + +- First, topologically sort all of the gates $G_1 , G_2 , . . . , G_n$. + We’ll denote the output of gate $G_i$ as $x_i$, and denote Alice and + Bob’s shares of $x_i$ as $a_i$ and $b_i$ respectively. Our goal is for + Alice and Bob to jointly compute all of their $a_i$ and $b_i$ shares, + going gate-by-gate. + - We’ll also handle the inputs as follows: first, for the purposes of + this algorithm, we will consider the input bits to be gates as well. + $A$’s input bits will be the first $s$ "gates", and $B$’s input bits + will be the next $t$ "gates." For the first $s$ gates, $a_i = x_i$ + and $b_i = 0$. For the next $t$ gates, $b_i = x_i$ and $a_i = 0$. +- Now, we go gate-by-gate, starting at gate $G_(s + t + 1)$, and use a + lookup-OT procedure to enable Alice to compute $a_i$ and Bob to + compute $b_i$ as $i$ increments. There are two cases for any given + gate: + - If $G_i$ is a gate with a single input, then it’s a NOT gate. In + this case, $x_i = not x_j$ for some $j < i$. If $G_i$ is a gate of + this form, $A$ sets $a_i = not a_j$ and $B$ sets $b_i = b_j$. + - In the second case, Alice and Bob use the secret-share-gate OT + protocol described above. + +With this protocol, $A$ and $B$ can compute all values of $a_i$ and +$b_i$. At the end they simply reveal and xor their desired output bits +together! + +Note that we can reduce the number of OTs with a few optimizations. For +example, any time $♢$ is a linear operator (such as xor), $A$ and $B$ +can simply apply that linear operator to their respective shares. So we +only need to perform OTs for the nonlinear gates. + +== Yao’s Garbled Circuits: Replacing Per-Gate OTs With Encryption + +Though it might not seem like it on the surface, we’ve now arrived at +something that is actually quite close to +#link("https://vitalik.eth.limo/general/2020/03/21/garbled.html")[Yao’s garbled circuits protocol];. +In fact, Yao’s protocol can be seen as the above protocol with some +optimizations, that enable us to replace a bunch of OTs with +"precomputation" from Alice. + +OTs are expensive, and doing one OT per gate is still pretty inefficient +(though it’s certainly far better than an OT of size $2^s$!). Each OT +also requires a round of interaction between Alice and Bob, meaning that +this protocol involves many, many back-and-forths between Alice and Bob +(not ideal). + +A natural question to ask would be: is there a way that we can somehow +"batch" all of the OTs, or else perform some kind of precomputation so +that Alice and Bob can learn their secret shares at each gate +non-interactively? + +=== Observation: Alice never actually interacts with Bob + +This question becomes especially interesting in light of the following +observation: in our previous protocol, it turns out that Alice’s $a_i$ +values are selected completely independently of anything Bob does. In +other words, at the start of the protocol Alice could have simply +decided to write down all of her $a_i$’s in advance. + +In this view, Alice is essentially writing down a "scrambling" function +for the circuit trace at the very start of the protocol. Concretely, the +tuple ${ b_1 , . . . , b_n }$ that Bob computes over the course of the +protocol will end up being a scrambled version of the "true" trace +${ x_1 , . . . , x_n }$, where the scrambling is decided by Alice at the +beginning of the protocol. + +What does it mean to see Alice’s "scrambling" as a function, or set of +functions? Well, Alice is essentially defining a scrambling map $g_i$ +for each gate (including the input "gates"). For each gate $G_i$, the +scrambling map $g_i$ takes ${ 0 , 1 }$ to some random shuffling of +${ 0 , 1 }$. Specifically: + +- If $a_i = 0$, then $g_i (0) = 0$ and $g_i (1) = 1$ +- If $a_i = 1$, then $g_i (0) = 1$ and $g_i (1) = 0$ + +So, to sum up this alternative view of our latest protocol: Alice writes +down a bunch of scrambling functions $g_i$ at the start of the protocol, +and then over the course of the protocol Bob gradually learns the +scrambled version of the true circuit trace $x_i$. In particular, he’s +learning $b_i = g_i (x_i)$ for all $i$. + +=== Replacing OTs with Encryption + +Currently, Bob learns his "scrambled" values through OT. If we want to +find a way to remove the requirement of "one OT per gate," we need to +figure out a way for Bob to learn the scrambled trace non-interactively. + +This seems plausible. Alice shouldn’t really need to interact with Bob +at all, until the very end of the protocol. She wrote down the +scrambling at the beginning of the protocol, and she’s not supposed to +learn anything about Bob’s values or what he’s doing anyway; so it seems +like it might be possible for Alice to simply send Bob some information +that allows him to figure out his $b_i$’s without too much more +interaction with her. + +Let’s try to do this in the most direct way. For a gate $G_i$ that +represents the intermediate calculation $x_i = x_j ♢ x_k$, Alice might +publish a table that looks something like the following for Bob: + +#figure( + align(center)[#table( + columns: (32.32%, 67.68%), + align: (auto,auto,), + table.header([$b_j = g_j (x_j) , b_k = g_k (x_k)$], [$b_i = g_i (x_i)$],), + table.hline(), + [00], [$"Enc"_1 \( (a_j ♢ a_k) xor a_i$)], + [01], [$"Enc"_2 ((a_j ♢ not a_k) xor a_i)$], + [10], [$"Enc"_3 ((not a_j ♢ a_k) xor a_i)$], + [11], [$"Enc"_4 ((not a_j ♢ not a_k) xor a_i)$], + )] + , kind: table + ) + +(This table is the same as the lookup table from above, just with the +$b$’s written as outputs of scrambling functions, and with the values in +the second column "locked" by encryption). + +This table should be constructed so that Bob can only "unlock" the +appropriate value of $g_i (x_i)$ for the row of $g_j (x_j) , g_k (x_k)$ +values that he actually possesses–this simulates the OT property that +Bob can’t learn the values of any other rows. For example, $"Enc"_1$ +should only be possible for Bob to decrypt if $g_j (x_j) = 0$ and +$g_k (x_k) = 0$. + +But it turns out that there’s a very direct way to do this! We simply +have our scrambling $g_j$ and $g_k$ #emph[also] output some symmetric +encryption keys picked by Alice at the start of the protocol, in +addition to the scrambled bit. The values in the second column should be +encrypted with both of these keys, and the output of $g_i$ should also +include a new symmetric encryption key as well that can be used to +unlock later values. + +For example, suppose that for some value $x_j$ we have +$g_j (x_j) = (1 , sans(k e y)_(j 1))$, and for some value of $x_k$ we +have $g_k (x_k) = (0 , sans(k e y)_(k 0))$. Then $"Enc"_3$ is symmetric +encryption with a key derived from $sans(k e y)_(j 1)$ and +$sans(k e y)_(k 0)$ - for example, +$"Hash" (sans(k e y)_(j 1) , sans(k e y)_(k 0))$. + +In other words, the output of our "scrambling" function $g_i$ should +actually be a #emph[tuple] of two values: a scrambled bit, and a +symmetric encryption key that can be used to decrypt values in lookup +tables deeper in the circuit, where outputs of $g_i$ are involved: + +$g_i (0) = (a_i , sans(k e y)_(i a_i))$ +$g_i (1) = (not a_i , sans(k e y)_(i (not a_i)))$ + +Now, for each $i$, Bob learns both the scrambled bit of $g_i (x_i)$, as +well as a symmetric encryption key that can be used to decrypt future +outputs for which $x_i$ is an input. At the very end, Bob asks Alice to +reveal the scrambling functions for the final circuit outputs, and then +unscrambles his output bits. + +It turns out that there are still a few more kinks to iron out, like +handling inputs–for example, we’ll find that our lazy default of setting +$a_i = x_i , b_i = 0$ for Alice’s inputs and $b_i = x_i , a_i = 0$ for +Bob’s inputs won’t work anymore. But we’ve essentially arrived at what +we now know today as Yao’s Garbled Circuits protocol! In the Garbled +Circuits protocol, our operation of "scrambling" is instead called +"garbling." + +#strong[Final exercise for the reader];: Work out the remaining kinks to +arrive at a complete Garbled Circuits protocol. diff --git a/easy/src/mpc2.typ b/easy/src/mpc2.typ new file mode 100644 index 0000000..010ea6d --- /dev/null +++ b/easy/src/mpc2.typ @@ -0,0 +1,104 @@ +#import "preamble.typ":* + += Garbled Circuits and Two-party Computation + +Imagine Alice and Bob each have some secret values +$a$ and $b$, and would like to jointly compute some function $f$ over +their respective inputs. Furthermore, they’d like to keep their secret +values hidden from each other: if Alice and Bob follow the protocol +honestly, they should both end up learning the correct value of +$f (a , b)$, but Alice shouldn’t learn anything about $b$ (other than +what could be learned by knowing both $a$ and $f (a , b)$), and likewise +for Bob. + +Yao’s Garbled Circuits is one of the most well-known 2PC protocols +(Vitalik has a great explanation +#link("https://vitalik.eth.limo/general/2020/03/21/garbled.html")[here];). +The protocol is quite clever, and optimized variants of the protocol are +being +#link("https://github.com/privacy-scaling-explorations/mpz/tree/dev/garble")[implemented and used today];. + +== The Problem + +Here is our problem setting, slightly more formally: + +- $A$ knows a secret bitstring $a$ of length $s$ bits +- $B$ knows a secret bitstring $b$ of length $t$ bits +- $C$ is a binary circuit, which takes in $s + t$ bits, and runs them + through some $n$ gates. The outputs of some of the gates are the + public outputs of the circuit. Without loss of generality, let’s also + suppose that each gate in $C$ accepts either $1$ or $2$ input bits, + and outputs a single output bit. +- $A$ and $B$ would like to jointly compute $C (a , b)$ without + revealing to each other their secrets. + +== Garbled gates + +Our garbled circuits are going to be built out of "garbled gates". +A garbled gate is like a traditional gate (like AND, OR, NAND, NOR), +except its functionality is hidden. + +What does that mean? + +Let's say the gate has two input bits, +so there are four possible inputs to the gate: +$(0, 0), (0, 1), (1, 0), (1, 1)$. +For each of those four inputs $(x, y)$, +there is a secret password $P_(x, y)$. +The gate $G$ will only reveal its value $G(x, y)$ +if you give it the password $P_(x, y)$. + +It's easy to see how to make a garbled gate. +Choose a symmetric-key +[#footnote("Symmetric-key encryption is probably " + +"what you think of " + +"when you think of plain-vanilla encryption: " + +"You use a secret key $K$ to encrypt a message $m$, " + +"and then you use the same secret key $K$ to decrypt it.") +] +encryption scheme $Enc$ +[#footnote("We'll talk later about what sort of " + +"encryption scheme is suitable for this...")] +and publish the following table: +#table( + columns: 2, + [$(0, 0)$], [$Enc_(P_(0, 0))(G(0, 0))$], + [$(0, 1)$], [$Enc_(P_(0, 1))(G(0, 1))$], + [$(1, 0)$], [$Enc_(P_(1, 0))(G(1, 0))$], + [$(1, 1)$], [$Enc_(P_(1, 1))(G(1, 1))$], +) + +If you have the values $x$ and $y$, +and you know the password $P_(x, y)$, +you just go to the $(x, y)$ row of the table, +look up +$ + Enc_(P_(x, y))(G(x, y)), +$ +decrypt it, and learn $G(x, y)$. + +But if you don't know the password $P_(x, y)$, +assuming $Enc$ is a suitably secure encryption scheme, +you won't learn anything about the value $G_(x, y)$ +from its encryption. + +== Chaining garbled gates + +The next step to combine a bunch of garbled gates into a circuit. +We'll need to make two changes to the protocol. + +To chain garbled gates together, +we need to modify the output of each gate: +In addition to outputting the bit $z = G_i (x, y)$, +the $i$-th gate $G_i$ +will also output a password $P_z$ that Bob can use at the next step. +#todo[Introduce Bob] + +Now there's a problem here! +We said before that each gate should require four passwords +$ + P_(0, 0), P_(0, 1), P_(1, 0), P_(1, 1), +$ +one for each possible input pair of bits. + + diff --git a/easy/src/ot.typ b/easy/src/ot.typ new file mode 100644 index 0000000..1894d76 --- /dev/null +++ b/easy/src/ot.typ @@ -0,0 +1,152 @@ +#import "preamble.typ":* + += Oblivious Transfer + +Alice has $n$ messages $x_1, dots, x_n$. +Bob wants to request the $i$-th message, +without letting Alice learn anything about the value of $i$. +Alice wants to send Bob $x_i$, +without letting him learn anything about the other $n-1$ messages. +This process is called "oblivious transfer": +Alice transfers a single message to Bob, +but she remains oblivious as to which message she has transferred. + +We'll see two simple protocols to achieve this. + +== Commutative encryption + +Let's imagine that Alice and Bob +have access to some encryption scheme that is _commutative_: +$ + Dec_{B}( Dec_{A} + ( Enc_{B} + ( Enc_{A}(x) ) ) ) + = x. +$ + +In other words, if Alice encrypts a message, +and Bob applies a second-layer of encryption to the encrypted message, +it doesn't matter which order Alice and Bob decrypt the message in -- +they will still get the original message back. + +A metaphor for commutative encryption +is a box that's locked with two padlocks. +Alice puts a message inside the box, +lock it with her lock, and ship it to Bob. +Bob puts his own lock back on the box and ships it back to Alice. +What's special about commutative encryption +is that Bob's lock doesn't block Alice from unlocking her own -- +so Alice can remove her lock and send it back to Bob, +and then Bob removes his lock and recovers the message. + +Mathematically, you can get commutative encryption +by working in a finite group (for example $ZZ_p^*$, or an elliptic curve). + +Alice's secret key is an integer $a$; +she encrypts a message $g$ by raising it to the $a$-th power, +and she sends Bob $g^a$. + +Bob encrypts again with his own secret key $b$, +and he sends $(g^a)^b = g^{a b}$ back to Alice. + +Now Alice removes her lock by taking an $a$-th root. The result is $g^b$, which she sends back to Bob. And Bob takes another $b$-th root, recovering $g$. + +== OT using commutative encryption + +Our first oblivious transfer protocol is built on the commutative encryption we just described. + +Alice has $n$ messages $x_1, dots, x_n$, which we may as well assume are elements of the group $G$. Alice chooses a secret key $a$, encrypts each message, and sends all $n$ ciphertexts to Bob: +$ + Enc_{a}(x_1), dots, Enc_{a}(x_n). +$ + +But crucially, Alice sends the ciphertexts in order, so Bob knows which is which. + +At this point, Bob can't read any of the messages, +because he doesn't know the keys. +No problem! +Bob just picks out the $i$-th ciphertext $Enc_{a}(x_i)$, +adds his own layer of encryption onto it, +and sends the resulting doubly-encoded message back to Alice: +$ + Enc_{b}(Enc_{a}(x_i)). +$ + +Alice doesn't know Bob's key $b$, +so she can't learn anything about the message he encrypted -- +even though it originally came from her. +Nonetheless she can apply her own decryption method +$Dec_a$ to it. +Since the encryption scheme is commutative, +the result of Alice's decryption is simply +$ + Enc_{b}(x_i), +$ +which she sends back to Bob. + +And Bob decrypts the message to learn $x_i$. + +== OT in one step + +The protocol above required one and a half rounds of communication: +Alice sent two messages to Bob, and Bob sent one message back to Alice. + +We can do better, using public-key cryptography. + +Let's start with a simplified protocol that is not quite secure. +The idea is for Bob to send Alice $n$ keys +$ +b_1, dots, b_n. +$ + +One of the $n$, say $b_i$, is a public key for which Bob knows the private key. The other $n-1$ are random garbage. + +Alice then uses one key to encrypt each message, and sends back to Bob: +$ +Enc_{b_1}(x_1), dots, Enc_{b_n}(x_n). +$ + +Now Bob uses the private key for $b_i$ to decrypt $x_i$, and he's done. + +Is Bob happy with this protocol? Yes. +Alice has no way of learning the value of $i$, +as long as she can't distinguish a true public key +from a random fake key (which is true of public-key schemes in practice). + +But is Alice happy with it? Not so much. +A cheating Bob could send $n$ different public keys, +and Alice has no way to detect it -- +like we just said, Alice can't tell random garbage from a true public key! +And then Bob would be able to decrypt all $n$ messages $x_1, dots, x_n$. + +But there's a simple trick to fix it. +Bob chooses some "verifiably random" value $r$ -- +to fix ideas, we could agree to use $r = sha(1)$. +Then we require that the numbers $b_1, dots, b_n$ +form an arithmetic progression with common difference $r$. + Bob chooses $i$, computes a public-private key pair, + and sets $b_i$ equal to that key. + Then all the other terms $b_1, dots, b_n$ + are determined by the arithmetic progression requirement $b_j = b_i + (j-i)r$. + (Or if the keys are elements of a group in multiplicative notation, + we could write this as $b_j = r^{j-i} * b_i$.) + +Is this secure? +If we think of the hash function as a random-number generator, +then all $n-1$ "garbage keys" are effectively random values. +So now the question is: +Can Bob compute a private key for a given (randomly generated) public key? +It's a standard assumption in public-key cryptography that Bob can't do this: +there's no algorithm that reads in a public key and spits out the corresponding private key. +(Otherwise, the whole enterprise is doomed.) +So Alice is guaranteed that Bob only knows how to decrypt (at most) one message. + +In fact, some public-key cryptosystems (like ElGamal) +have a sort of "homomorphic" property: +If you know the private keys for to two different public keys $b_1$ and $b_2$, +then you can compute the private key for the public key $b_2 b_1^{-1}$. +(In ElGamal, this is true because the private key is just a discrete logarithm.) +So, if Bob could dishonestly decrypt two of Alice's messages, +he could compute the private key for the public key $r$. +But $r$ is verifiably random, +and it's very hard (we assume) for Bob to find a private key for a random public key. diff --git a/easy/src/pair.typ b/easy/src/pair.typ new file mode 100644 index 0000000..a6459d0 --- /dev/null +++ b/easy/src/pair.typ @@ -0,0 +1,114 @@ +#import "preamble.typ":* + += Bilinear pairings on elliptic curves + + + +The map $[bullet] : FF_q -> E$ is linear, +meaning that $[a + b] = [a] + [b]$, and $[n a] = n[a]$. +But as written we can't do "armored multiplication": + +#claim[ + As far as we know, given $[a]$ and $[b]$, one cannot compute $[a b]$. +] + +On the other hand, it _does_ turn out that we know a way to _verify_ +a claimed answer on certain curves. +That is: +#proposition[ + On the curve BN254, given three points $[a]$, $[b]$, and $[c]$ on the curve, + one can verify whether $a b = c$. +] + +The technique needed is that one wants to construct a +nondegenerate bilinear function +$ pair : E times E -> ZZ slash N ZZ $ +for some large integer $N$. +I think this should be called a *bilinear pairing*, +but for some reason everyone justs says *pairing* instead. +A curve is called *pairing-friendly* if this pairing can be computed reasonably +(e.g. BN254 is pairing-friendly, but Curve25519 is not). + +This construction actually uses some really deep graduate-level number theory +(in contrast, all the math in @ec is within an undergraduate curriculum) +that is well beyond the scope of these lecture notes. +Fortunately, we won't need the details of how it works; +but we'll comment briefly in @pairing-friendly on what curves it can be done on. +And this pairing algorithm needs to be worked out just once for the curve $E$; +and then anyone in the world can use the published curve for their protocol. + +Going a little more generally, the four-number equation +$ pair([m], [n]) = pair([m'], [n']) $ +will be true whenever $m n = m' n'$, +because both sides will equal $m n pair([1], [1])$. +So this gives us a way to _verify_ two-by-two multiplication. + +#remark[ + The last sentence is worth bearing in mind: in all the protocols we'll see, + the pairing is only used by the _verifier_ Victor, never by the prover Peggy. +] + +#remark[We don't know how to do multilinear pairings][ + On the other hand, we currently don't seem to know a good + way to do _multilinear_ pairings. + For example, we don't know a good trilinear map + $E times E times E -> ZZ slash N ZZ$ + that would allow us to compare $[a b c]$, $[a]$, $[b]$, $[c]$ + (without already knowing one of $[a b]$, $[b c]$, $[c a]$). +] + +== Verifying more complicated claims + +#example[ + Suppose Peggy wants to convince Victor that $y = x^3 + 2$, + where Peggy has sent Victor elliptic curve points [x] and [y]. + To do this, Peggy additionally sends to Victor $[x^2]$ and $[x^3]$. + + Given $[x]$, $[x^2]$, $[x^3]$, and $[y]$, + Victor verifies that: + - $pair([x^2], 1) = pair([x], [x]) $ + - $pair([x^3], 1) = pair([x^2], [x]) $ + - $[y] = [x^3] + 2 [1]$. + + The process of verifing this sort of identity is quite general: + The prover sends intermediate values as needed + so that the verifier can verify the claim using only pairings and linearity. +] + + +== So which curves are pairing-friendly? + +If we chose $E$ to be BN254, the following property holds: + +#proposition[ + For $(p,q)$ as in BN254, + the smallest integer $k$ such that $q$ divides $p^k-1$ is $k=12$. +] + +This integer $k$ is called the *embedding degree*. +This section is an aside explaining how the embedding degree affects pairing. + +The pairing function $pair(a, b)$ takes as input two points $a, b in E$ +on the elliptic curve, +and spits out a value $pair(a, b) in FF_{p^k}^*$ -- +in other words, a nonzero element of the finite field of order $p^k$ +(where $k$ is the embedding degree we just defined). +In fact, this element will always be a $q$th root of unity in $FF_{p^k}$, +and it will satisfy $pair([m], [n]) = zeta^{m n}$, +where $\zeta$ is some fixed $q$th root of unity. +The construction of the pairing is based on the +#link("https://en.wikipedia.org/wiki/Weil_pairing", "Weil pairing"). +in algebraic geometry. +How to compute these pairings is well beyond the scope of these notes; +the raw definition is quite abstract, +and a lot of work has gone into computing the pairings efficiently. +(For more details, see these +#link("https://crypto.stanford.edu/pbc/notes/ep/pairing.html", "notes").) + +The difficulty of computing these pairings is determined by the size of $k$: +the values $pair(a, b)$ will be elements of a field of size $p^k$, +so they will require 256 bits even to store. +For a curve to be "pairing-friendly" -- in order to be able to +do pairing-based cryptography on it -- we need the value of $k$ to be pretty small. + + diff --git a/easy/src/plonk.typ b/easy/src/plonk.typ new file mode 100644 index 0000000..32c10ca --- /dev/null +++ b/easy/src/plonk.typ @@ -0,0 +1,495 @@ +#import "preamble.typ":* + +#let rbox(s) = [#text(red)[#ellipse(stroke: red, inset: 2pt, s)]] +#let bbox(s) = [#text(blue)[#rect(stroke: blue, inset: 4pt, s)]] + += PLONK, a zkSNARK protocol + +For this section, one can use any polynomial commitment scheme one prefers. +So we'll introduce the notation $Com(P)$ for the commitment of a polynomial +$P(X) in FF_q [X]$, with the understanding that either KZG, IPA, or something +else could be in use here. + +== Root check (using long division with commitment schemes) + +Both commitments schemes allow for a technique I privately call _root-check_ here. +Here's the problem statement: + +#problem[ + Suppose one had two polynomials $P_1$ and $P_2$, + and Peggy has given commitments $Com(P_1)$ and $Com(P_2)$. + Peggy would like to prove to Victor that, say, + the equation $P_1(z) = P_2(z)$ for all $z$ in some large finite set $S$. +] + +Of course, Peggy could open $Com(P_1)$ and $Com(P_2)$ at every point in $S$. +But there are some situations in which Peggy still wants to prove this +without actually revealing the common values of the polynomial for any $z in S$. +Even when $S$ is a single number (i.e. Peggy wants to show $P_1$ and $P_2$ agree +on a single value without revealing the common value), +it's not obvious how to do this. + +Well, it turns out we can basically employ the same technique as in @kzg. +Peggy just needs to show is that $P_1-P_2$ +is divisible by $Z(X) := product_(z in S) (X-z)$. +This can be done by committing the quotient $H(X) := (P_1(X) - P_2(X)) / Z(X)$. +Victor then gives a random challenge $lambda in FF_q$, +and then Peggy opens $Com(P_1)$, $Com(P_2)$, and $Com(H)$ at $lambda$. + +But we can actually do this more generally with _any_ polynomial +expression $F$ in place of $P_1 - P_2$, +as long as Peggy has a way to prove the values of $F$ are correct. +As an artificial example, if Peggy has sent Victor $Com(P_1)$ through $Com(P_6)$, +and wants to show that +$ P_1(42) + P_2(42) P_3(42)^4 + P_4(42) P_5(42) P_6(42) = 1337, $ +she could define $F(X) = P_1(X) + P_2(X) P_3(X)^4 + P_4(X) P_5(X) + P_6(X)$ +and run the same protocol with this $F$. +This means she doesn't have to reveal any $P_i (42)$, which is great! + +To be fully explicit, here is the algorithm: + +#algorithm[Root-check][ + Assume that $F$ is a polynomial for which + Peggy can establish the value of $F$ at any point in $FF_q$. + Peggy wants to convince Victor that $F$ vanishes on a given finite set $S subset.eq FF_q$. + + 1. Both parties compute the polynomial + $ Z(X) := product_(z in S) (X-z) in FF_q [X]. $ + 2. Peggy does polynomial long division to compute $H(X) = F(X) / Z(X)$. + 3. Peggy sends $Com(H)$. + 4. Victor picks a random challenge $lambda in FF_q$ + and asks Peggy to open $Com(H)$ at $lambda$, + as well as the value of $F$ at $lambda$. + 5. Victor verifies $F(lambda) = Z(lambda) H(lambda)$. +] + +== Arithmetization + +The promise of programmable cryptography is that we should be able to +perform zero-knowledge proofs for arbitrary functions. +That means we need a "programming language" that we'll write our function in. + +For PLONK (and Groth16 in the next section), the choice that's used is: +*systems of quadratic equations over $FF_q$*. + +This leads to the natural question of how a function like SHA256 can be encoded +into a system of quadratic equations. +Well, quadratic equations over $FF_q$, +viewed as an NP-problem called Quad-SAT, is pretty clearly NP-complete, +as the following example shows: + +#remark([Quad-SAT is pretty obviously NP-complete])[ + If you can't see right away that Quad-SAT is NP-complete, + the following example instance can help, + showing how to convert any instance of 3-SAT into a Quad-SAT problem: + $ + x_i^2 &= x_i #h(1em) forall 1 <= i <= 1000 & \ + y_1 &= (1-x_(42)) dot x_(17), & #h(1em) & 0 = y_1 dot x_(53) & \ + y_2 &= (1-x_(19)) dot (1-x_(52)) & #h(1em) & 0 = y_2 dot (1-x_(75)) & \ + y_3 &= x_(25) dot x_(64), &#h(1em) & 0 = y_3 dot x_(81) & \ + &dots.v + $ + (imagine many more such pairs of equations). + The $x_i$'s are variables which are seen to either be $0$ or $1$. + And then each pair of equations with $y_i$ corresponds to a clause of 3-SAT. +] + +So for example, any NP decision problem should be encodable. +Still, such a theoretical reduction might not be usable in practice: +polynomial factors might not matter in complexity theory, +but they do matter a lot to engineers and end users. +Just having a #link("https://w.wiki/5z5Z", "galactic algorithm") isn't enough. + +But it turns out that Quad-SAT is actually reasonably code-able. +This is the goal of projects like +#link("https://docs.circom.io/", "Circom"), +which gives a high-level language that compiles a function like SHA-256 +into a system of equations over $FF_q$ that can actually be used in practice. +Systems like this are called *arithmetic circuits*, +and Circom is appropriately short for "circuit compiler". +If you're curious, you can see how SHA256 is implemented in Circom on +#link("https://github.com/iden3/circomlib/blob/master/circuits/sha256/sha256.circom", +"GitHub"). + +To preserve continuity of the mathematics, +we'll defer further discussion of coding in quadratic equations to later. + +== An instance of PLONK + +For PLONK, the equations are standardized further to a certain form: + +#definition[ + An instance of PLONK consists of two pieces, + the *gate constraints* and the *copy constraints*. + + The *gate constraints* are a system of $n$ equations, + $ q_(L,i) a_i + q_(R,i) b_i + q_(O,i) c_i + q_(M,i) a_i b_i + q_(C,i) = 0 $ + for $i = 1, ..., n$, + in the $3n$ variables $a_i$, $b_i$, $c_i$. + while the $q_(*,i)$ are coefficients in $FF_q$, which are globally known. + The confusing choice of subscripts stands for "Left", "Right", "Output", + "Multiplication", and "Constant", respectively. + + The *copy constraints* are a bunch of assertions that some of the + $3n$ variables should be equal to each other, + so e.g. "$a_1 = c_7$", "$b_17 = b_42$", and so on. +] + +So the PLONK protocol purports to do the following: +Peggy and Victor have a PLONK instance given to them. +Peggy has a solution to the system of equations, +i.e. an assignment of values to each $a_i$, $b_i$, $c_i$ such that +all the gate constraints and all the copy constraints are satisfied. +Peggy wants to prove this to Victor succinctly +and without revealing the solution itself. +The protocol then proceeds by having: + +1. Peggy sends a polynomial commitment corresponding to $a_i$, $b_i$, and $c_i$ + (the details of what polynomial are described below). +2. Peggy proves to Victor that the commitment from Step 1 + satisfies the gate constraints. +3. Peggy proves to Victor that the commitment from Step 1 + also satisfies the copy constraints. + +Let's now explain how each step works. + +== Step 1: The commitment + +In PLONK, we'll assume that $q equiv 1 mod n$, which means that +we can fix $omega in FF_q$ to be a primitive $n$th root of unity. + +Then, by polynomial interpolation, Peggy chooses polynomials $A(X)$, $B(X)$, +and $C(X)$ in $FF_q [X]$ such that +#eqn[ + $ A(omega^i) = a_i, #h(1em) B(omega^i) = b_i, #h(1em) C(omega^i) = c_i #h(1em) + " for all " i = 1, 2, ..., n. $ + +] +We specifically choose $omega^i$ because that way, +if we use @root-check on the set ${omega, omega^1, ..., omega^n}$, +then the polynomial called $Z$ is just +$Z(X) = (X-omega) ... (X-omega^n) = X^n-1$, which is really nice. +In fact, often $n$ is chosen to be a power of $2$ so that $A$, $B$, and $C$ +are really easy to compute, using a fast Fourier transform. +(Note: When you're working in a finite field, the fast Fourier transform +is sometimes called the "number theoretic transform" (NTT) +even though it's exactly the same as the usual FFT.) + +Then: +#algorithm("Commitment step of PLONK")[ + 1. Peggy interpolates $A$, $B$, $C$ as in @plonk-setup. + 2. Peggy sends $Com(A)$, $Com(B)$, $Com(C)$ to Victor. +] +To reiterate, each commitment is a 256-bit +that can later be "opened" at any value $x in FF_q$. + +== Step 2: Gate-check + +Both Peggy and Victor knows the PLONK instance, +so they can interpolate a polynomial +$Q_L(X) in FF_q [X]$ of degree $n-1$ such that +$ Q_L (omega^i) = q_(L,i) #h(1em) " for " i = 1, ..., n. $ +Then the analogous polynomials $Q_R$, $Q_O$, $Q_M$, $Q_C$ +are defined in the same way. + +Now, what do the gate constraints amount to? +Peggy is trying to convince Victor that the equation +#eqn[ + $ Q_L (x) A (x) + Q_R (x) B (x) + Q_O (x) C (x) + + Q_M (x) A (x) B (x) + Q_C (x) = 0 $ + +] +is true for the $n$ numbers $x = 1, omega, omega^2, ..., omega^(n-1)$. + +However, Peggy has committed $A$, $B$, $C$ already, +while all the $Q_*$ polynomials are globally known. +So this is a direct application of @root-check: + +#algorithm[Gate-check][ + 1. Both parties interpolate five polynomials $Q_* in FF_q [X]$ + from the $15n$ coefficients $q_*$ + (globally known from the PLONK instance). + 2. Peggy uses @root-check to convince Victor that @plonk-gate + holds for $X = omega^i$ + (that is, the left-hand side is is indeed divisible by $Z(X) := X^n-1$). +] + + +/* +However, that's equivalent to the _polynomial_ +$ Q_L (X) A_i (X) + Q_R (X) B_i (X) + Q_O (X) C_i (X) + + Q_M (X) A_i (X) B_i (X) + Q_C (X) in FF_q [X] $ +being divisible by the degree $n$ polynomial +$ Z(X) = (X-omega)(X-omega^2) ... (X-omega^n) = X^n - 1. $ + +In other words, it suffices for Peggy to convince Victor that there +is a polynomial $H(X) in FF_q [X]$ such that +#eqn[ + $ Q_L (X) A_i (X) &+ Q_R (X) B_i (X) + Q_O (X) C_i (X) \ + &+ Q_M (X) A_i (X) B_i (X) + Q_C (X) = Z(X) H(X). $ + +] + +And this can be done using polynomial commitments pretty easily: +Peggy should send $Com(H)$, +and then Victor just verifies @plonkpoly at random values in $FF_q$. +As both sides are polynomials of degree up to $3(n-1)$, +either the equation holds for every input +or there are at most $3n-4$ values for which it's true +(two different polynomials of degree $3(n-1)$ can agree at up to $3n-4$ points). + +#algorithm("Proving PLONK satisfies the gate constraints")[ + 1. Peggy computes $H(X) in FF_q [X]$ using polynomial long division + and sends $Com(H)$ to Victor. + 2. Victor picks a random challenge and asks Peggy to open + all of $Com(A)$, $Com(B)$, $Com(C)$, $Com(H)$ at that challenge. + 3. Victor accepts if and only if @plonkpoly is true at the random challenge. +] +*/ + +== Step 3: Proving the copy constraints + +The copy constraints are the trickier step. +There are a few moving parts to this idea, so to ease into it slightly, +we provide a solution to a "simpler" problem called "permutation-check". +Then we explain how to deal with the full copy check. + +=== Easier case: permutation-check + +So let's suppose we have polynomials $P, Q in FF_q [X]$ +which are encoding two vectors of values +$ arrow(p) &= angle.l P(omega^1), P(omega^2), ..., P(omega^n) angle.r \ + arrow(q) &= angle.l Q(omega^1), Q(omega^2), ..., Q(omega^n) angle.r. $ +Is there a way that one can quickly verify $arrow(p)$ and $arrow(q)$ +are the same up to permutation of the $n$ entries? + +Well, actually, it would be necessary and sufficient for the identity +#eqn[ + $ (T+P(omega^1))(T+P(omega^2)) ... (T+P(omega^n)) + = (T+Q(omega^1))(T+Q(omega^2)) ... (T+Q(omega^n)) $ + +] +to be true, in the sense both sides are the same polynomial in $FF_q [T]$ +in a single formal variable $T$. +And for that, it actually is sufficient that a single random challenge +$T = lambda$ passes @permcheck-poly; as if the two sides of @permcheck-poly +aren't the same polynomial, +then the two sides can have at most $n-1$ common values. + +We can then get a proof of @permcheck-poly +using the technique of adding an _accumulator polynomial_. +The idea is this: Victor picks a random challenge $lambda in FF_q$. +Peggy then interpolates the polynomial $F_P in FF_q [T]$ such that +$ + F_P (omega^1) &= lambda + P(omega^1) \ + F_P (omega^2) &= (lambda + P(omega^1))(lambda + P(omega^2)) \ + &dots.v \ + F_P (omega^n) &= (lambda + P(omega^1))(lambda + P(omega^2)) dots.c (lambda + P(omega^n)). +$ +Then the accumulator $F_Q in FF_q[T]$ is defined analogously. + +So to proev @permcheck-poly, the following algorithm works: + +#algorithm[Permutation-check][ + Suppose Peggy has committed $Com(P)$ and $Com(Q)$. + + 1. Victor sends a random challenge $lambda in FF_q$. + 2. Peggy interpolates polynomials $F_P [T]$ and $F_Q [T]$ + such that $F_P (omega^k) = product_(i <= k) (lambda + P(omega^i))$. + Define $F_Q$ similarly. + Peggy sends $Com(F_P)$ and $Com(F_Q)$. + 3. Peggy uses @root-check to prove all of the following statements: + + - $F_P (X) - (lambda + P(X))$ + vanishes at $X = omega$; + - $F_P (omega X) - (lambda + P(X)) F_P (X)$ + vanishes at $X in {omega, ..., omega^(n-1)}$; + - The previous two statements also hold with $F_P$ replaced by $F_Q$; + - $F_P (X) - F_Q (X)$ vanishes at $X = 1$. +] + +=== Copy check + +Moving on to copy-check, let's look at a concrete example where $n=4$. +Suppose that our copy constraints were +$ #rbox($a_1$) = #rbox($a_4$) = #rbox($c_3$) + #h(1em) "and" #h(1em) + #bbox($b_2$) = #bbox($c_1$). $ +(We've colored and circled the variables that will move around for readability.) +So, the copy constraint means we want the following equality of matrices: +#eqn[ + $ + mat( + a_1, a_2, a_3, a_4; + b_1, b_2, b_3, b_4; + c_1, c_2, c_3, c_4; + ) + = + mat( + #rbox($a_4$), a_2, a_3, #rbox($c_3$) ; + b_1, #bbox($c_1$), b_3, b_4; + #bbox($b_2$), c_2, c_3, #rbox($a_1$) + ) + . + $ + +] +Again, our goal is to make this into a _single_ equation. +There's a really clever way to do this by tagging each entry with $+ eta^j omega^k mu$ +in reading order for $j = 0, 1, 2$ and $k = 1, ..., n$; +here $eta in FF_q$ is any number such that $eta^2$ doesn't happen to be a power of $omega$, +so all the tags are distinct. +Specifically, if @copy1 is true, then for any $mu in FF_q$, we also have +#eqn[ + $ + & mat( + a_1 + omega^1 mu, a_2 + omega^2 mu, a_3 + omega^3 mu, a_4 + omega^4 mu; + b_1 + eta omega^1 mu, b_2 + eta omega^2 mu, b_3 + eta omega^3 mu, b_4 + eta omega^4 mu; + c_1 + eta^2 omega^1 mu, c_2 + eta^2 omega^2 mu, c_3 + eta^2 omega^3 mu, c_4 + eta^2 omega^4 mu; + ) \ + =& + mat( + #rbox($a_4$) + omega^1 mu, a_2 + omega^2 mu, a_3 + omega^3 mu, #rbox($c_3$) + omega^4 mu; + b_1 + eta omega^1 mu, #bbox($c_1$) + eta omega^2 mu, b_3 + eta omega^3 mu, b_4 + eta omega^4 mu; + #bbox($b_2$) + eta^2 omega^1 mu, c_2 + eta^2 omega^2 mu, c_3 + eta^2 omega^3 mu, #rbox($a_1$) + eta^2 omega^4 mu; + ) + . + $ + +] +Now how can the prover establish @copy2 succinctly? +The answer is to run a permutation-check on the $3n$ entries of @copy2! +Because $mu$ was a random challenge, +one can really think of each binomial above more like an ordered pair: +for almost all challenges $mu$, there are no "unexpected" equalities. +In other words, up to a negligible number of $mu$, +@copy2 will be true if and only if the right-hand side is just +a permutation of the left-hand side. + +To clean things up, shuffle the $12$ terms on the right-hand side of @copy2 +so that each variable is in the cell it started at: +#eqn[ + $ + "Want to prove" & mat( + a_1 + omega^1 mu, a_2 + omega^2 mu, a_3 + omega^3 mu, a_4 + omega^4 mu; + b_1 + eta omega^1 mu, b_2 + eta omega^2 mu, b_3 + eta omega^3 mu, b_4 + eta omega^4 mu; + c_1 + eta^2 omega^1 mu, c_2 + eta^2 omega^2 mu, c_3 + eta^2 omega^3 mu, c_4 + eta^2 omega^4 mu; + ) \ + "is a permutation of" & + mat( + a_1 + #rbox($eta^2 omega^4 mu$), a_2 + omega^2 mu, a_3 + omega^3 mu, a_4 + #rbox($omega^1 mu$) ; + b_1 + eta omega^1 mu, b_2+ #bbox($eta^2 omega^1 mu$), b_3 + eta omega^3 mu, b_4 + eta omega^4 mu ; + b_1 + #bbox($eta omega^2 mu$), c_2 + eta^2 omega^2 mu, c_3 + eta^2 omega^3 mu, c_4 + #rbox($omega^4 mu$) + ) + . + $ + +] +The permutations needed are part of the problem statement, hence globally known. +So in this example, both parties are going to interpolate cubic polynomials +$sigma_a, sigma_b, sigma_c$ that encode the weird coefficients row-by-row: +$ + mat( + delim: #none, + sigma_a (omega^1) = #rbox($eta^2 omega^4$), + sigma_a (omega^2) = omega^2, + sigma_a (omega^3) = omega^3, + sigma_a (omega^4) = #rbox($omega^1$) ; + sigma_b (omega^1) = eta omega^1, + sigma_b (omega^2) = #bbox($eta^2 omega^1$), + sigma_b (omega^3) = eta omega^3, + sigma_b (omega^4) = eta omega^4 ; + sigma_c (omega^1) = #bbox($eta omega^2$), + sigma_c (omega^2) = eta^2 omega^2, + sigma_c (omega^3) = eta^2 omega^3, + sigma_c (omega^4) = #rbox($omega^4$). + ) +$ +Then one can start defining accumulator polynomials, after +re-introducing the random challenge $lambda$ from permutation-check. +We're going to need six in all, three for each side of @copy3: +we call them $F_a$, $F_b$, $F_c$, $F_a'$, $F_b'$, $F_c'$. +The ones on the left-hand side are interpolated so that +#eqn[ + $ + F_a (omega^k) &= product_(i <= k) (a_i + omega^i mu + lambda) \ + F_b (omega^k) &= product_(i <= k) (b_i + eta omega^i mu + lambda) \ + F_c (omega^k) &= product_(i <= k) (c_i + eta^2 omega^i mu + lambda) \ + $ + +] +whilst the ones on the right have the extra permutation polynomials +#eqn[ + $ + F'_a (omega^k) &= product_(i <= k) (a_i + sigma_a (omega^i) mu + lambda) \ + F'_b (omega^k) &= product_(i <= k) (b_i + sigma_b (omega^i) mu + lambda) \ + F'_c (omega^k) &= product_(i <= k) (c_i + sigma_c (omega^i) mu + lambda). + $ + +] +And then we can run essentially the algorithm from before. +There are six initialization conditions +#eqn[ + $ + F_a (omega^1) &= A(omega^1) + omega^1 mu + lambda \ + F_b (omega^1) &= B(omega^1) + eta omega^1 mu + lambda \ + F_c (omega^1) &= C(omega^1) + eta^2 omega^1 mu + lambda \ + F_a (omega^1) &= A(omega^1) + sigma_a (omega^1) mu + lambda \ + F_b (omega^1) &= B(omega^1) + sigma_b (omega^1) mu + lambda \ + F_c (omega^1) &= C(omega^1) + sigma_c (omega^1) mu + lambda. + $ + +] +and six accumulation conditions +#eqn[ + $ + F_a (omega X) &= F_a (X) dot (A(X) + X mu + lambda) \ + F_b (omega X) &= F_b (X) dot (B(X) + 2 X mu + lambda) \ + F_c (omega X) &= F_c (X) dot (C(X) + 3 X mu + lambda) \ + F'_a (omega X) &= F'_a (X) dot (A(X) + sigma_a (X) mu + lambda) \ + F'_b (omega X) &= F'_b (X) dot (B(X) + sigma_b (X) mu + lambda) \ + F'_c (omega X) &= F'_c (X) dot (C(X) + sigma_c (X) mu + lambda) \ + $ + +] +before the final product condition +#eqn[ + $ + F_a (1) F_b (1) F_c (1) = F'_a (1) F'_b (1) F'_c (1) + $ + +] + +To summarize, the copy-check goes as follows: +#algorithm[Copy-check][ + 1. Both parties compute the degree $n-1$ polynomials + $sigma_a, sigma_b, sigma_c in FF_q [X]$ described above, + based on the copy constraints in the problem statement. + 2. Victor chooses random challenges $mu, lambda in FF_q$ and sends them to Peggy. + 3. Peggy interpolates the six accumulator polynomials $F_a$, ..., $F'_c$ defined + in @copycheck-left and @copycheck-right. + 4. Peggy uses @root-check to prove @copycheck-init holds. + 5. Peggy uses @root-check to prove @copycheck-accum holds + for $X in {omega, omega^2, ..., omega^(n-1)}$. + 6. Peggy uses @root-check to prove @copycheck-final holds. +] + +== Public and private witnesses + +#todo[warning: $A$, $B$, $C$ should not be the lowest degree interpolations, imo +AV: why not? I think it's fine if they are] + +The last thing to be done is to reveal the value of public witnesses, +so the prover can convince the verifier that those values are correct. +This is simply an application of @root-check. +Let's say the public witnesses are the values $a_i$, for all $i$ in some set $S$. +(If some of the $b$'s and $c$'s are also public, we'll just do the same thing for them.) +The prover can interpolate another polynomial, $A^"public"$, +such that $A^"public"(omega^i) = a_i$ if $i in S$, and $A^"public"(omega^i) = 0$ if $i in.not S$. +Actually, both the prover and the verifier can compute $A^"public"$, since +all the values $a_i$ are publicly known! + +Now the prover runs @root-check to prove that $A - A^"public"$ +vanishes on $S$. +(And similarly for $B$ and $C$, if needed.) +And we're done. diff --git a/easy/src/preamble.typ b/easy/src/preamble.typ new file mode 100644 index 0000000..fe6ecc1 --- /dev/null +++ b/easy/src/preamble.typ @@ -0,0 +1,176 @@ +#let pair = math.op("pair") +#let sha = math.op("hash") +#let msg = math.sans("msg") +#let Com = math.op("Com") +#let Flatten = math.op("Flatten") +#let Enc = math.op("Enc") +#let Dec = math.op("Dec") +#let pk = math.sans("pk") +#let sk = math.sans("sk") + +// https://github.com/vEnhance/dotfiles/blob/main/typst/packages/local/evan/1.0.0/evan.typ +#import "@preview/ctheorems:1.1.2": * + +#let fonts = ( + text: ("Linux Libertine"), + sans: ("Noto Sans"), + mono: ("Inconsolata"), +) +#let colors = ( + title: eastern, + headers: maroon, +) + +#let toc = { + show outline.entry.where(level: 1): it => { + v(1.2em, weak:true) + text(weight:"bold", font:fonts.sans, it) + } + text(fill:colors.title, size:1.4em, font:fonts.sans, [*Table of contents*]) + v(0.6em) + outline( + title: none, + indent: 2em, + ) +} + +#let eqn(s) = { + set math.equation(numbering: "(1)") + s +} + +#let theorem = thmbox("main", "Theorem", fill: rgb("#ffeeee"), base_level: 1) +#let lemma = thmbox("main", "Lemma", fill: rgb("#ffeeee"), base_level: 1) +#let proposition = thmbox("main", "Proposition", fill: rgb("#ffeeee"), base_level: 1) +#let claim = thmbox("main", "Claim", fill: rgb("#ffeeee"), base_level: 1) +#let definition = thmbox("main", "Definition", fill: rgb("#ddddff"), base_level: 1) +#let example = thmbox("main", "Example", fill: rgb("#ffffdd"), base_level: 1) +#let algorithm = thmbox("main", "Algorithm", fill: rgb("#ddffdd"), base_level: 1) +#let remark = thmbox("main", "Remark", fill: rgb("#eeeeee"), base_level: 1) + +#let problem = thmplain("main", "Problem", base_level: 1) +#let exercise = thmplain("main", "Problem", base_level: 1) + +#let todo = thmbox("todo", "TODO", fill: rgb("#ddaa77")).with(numbering: none) + +#let proof = thmproof("proof", "Proof") + +#let assumption = thmbox("main", "Assumption", fill: rgb("#eeeeaa"), base_level: 1) +#let goal = thmbox("main", "Goal", fill: rgb("#eeeeaa"), base_level: 1) + +#let url(s) = { + link(s, text(font:fonts.mono, s)) +} +#let pmod(x) = $space (mod #x)$ + +// Main entry point to use in a global show rule +#let evan( + title: none, + author: none, + subtitle: none, + date: none, + maketitle: true, + body +) = { + // Set document parameters + if (title != none) { + set document(title: title) + } + if (author != none) { + set document(author: author) + } + + // General settings + set page( + paper: "a4", + margin: auto, + header: context { + set align(right) + set text(size:0.8em) + if (not maketitle or counter(page).get().first() > 1) { + text(weight:"bold", title) + if (author != none) { + h(0.2em) + sym.dash.em + h(0.2em) + text(style:"italic", author) + } + } + }, + numbering: "1", + ) + set par( + justify: true + ) + set text( + font:fonts.text, + size:11pt, + ) + + // Theorem environments + show: thmrules.with(qed-symbol: $square$) + + // Change quote display + set quote(block: true) + show quote: set pad(x:2em, y:0em) + show quote: it => { + set text(style: "italic") + v(-1em) + it + v(-0.5em) + } + + // Section headers + set heading(numbering: "1.1") + show heading: it => { + set text(font:fonts.sans) + block([ + #if (it.numbering != none) [ + #text(fill:colors.headers, "§" + counter(heading).display()) + #h(0.2em) + ] + #it.body + #v(0.4em) + ]) + } + + // Hyperlinks in blue text + show link: it => { + if (type(it.dest) == "label") { + set text(fill:red) + it + } else { + set text(fill:blue) + it + } + } + show ref: it => { + if (it.supplement == auto) { + link(it.target, it) + } else { + link(it.target, it.supplement) + } + } + + // Title page, if maketitle is true + if maketitle { + v(2.5em) + set align(center) + set block(spacing: 2em) + block(text(fill:colors.title, size:2em, font:fonts.sans, weight:"bold", title)) + if (subtitle != none) { + block(text(size:1.5em, font:fonts.sans, weight:"bold", subtitle)) + } + if (author != none) { + block(smallcaps(text(size:1.7em, author))) + } + if (type(date) == "datetime") { + block(text(size:1.2em, date.display("[day] [month repr:long] [year]"))) + } + else if (date != none) { + block(text(size:1.2em, date)) + } + v(1.5em) + } + body +}