Obfuscation (Part II): Diamond iO

2026 Jul 28 See all posts


Obfuscation (Part II): Diamond iO

Special thanks to Sora Suegami and Janmajaya Mall for feedback and review.

In the last part of this series, we went through the full tech tree of the most mainstream and conservative line of cryptographic obfuscation (iO) protocols. These protocols allow you to "encrypt" programs in such a way that anyone can run the "encrypted" program on plaintext inputs and get plaintext outputs, without being able to see the internal logic of the program. This can be used for all kinds of use cases, particularly situations where the program contains some secret key internally, and obfuscating accomplishes the goal of giving someone a package that lets them do some things with the secret key in some circumstances, but not anything else.

The biggest downside of these protocols so far has been their literally galactic runtime - if you actually try to figure out how long it would take to run one of them, you would get an answer longer than the lifetime of the universe. As a result, despite recent breakthroughs in feasibility, obfuscation protocols have so far been theoretical curiosities.

This post will describe in detail a different style of obfuscation: diamond iO (paper, presentation). This approach relies on much "braver" and more untested cryptographic assumptions, but it achieves having merely "planetary", rather than galactic runtime - still infeasible today, but perhaps only a few further optimizations away from becoming a reality at least for a few use cases.


Different types of obfuscation


How does diamond iO work?

At a high level, diamond iO is built by modifying the BGG+14 attribute-based encryption (ABE) scheme, explained in the previous post. I highly recommend re-reading that section before continuing.


It's also not this Abe. Bonus points if you know which Abe this one is - harder than the four in the last post!


Like, the more traditional iO protocols, diamond iO runs the computation in FHE inside of ABE, and then gives the evaluator a way to decrypt the result only if it's actually the outcome of running the computation correctly. But the way that diamond iO uses this machinery is different. First, it uses a completely different mechanism to do conditional FHE decryption. Second, it uses a completely different mechanism to generate the encodings for the input.

These two modifications are connected to each other, and are the real reason why diamond iO manages to be much more efficient - it's "just as computationally intensive as" functional encryption, instead of being a much more complicated tower on top of FE.

As a reminder, BGG+14 works by maintaining encodings of the form

\(s * (B - G * m) + e\)

Where:

(alternatively, you can add \(G * m\) instead of subtracting; both give equally valid and efficient schemes)

Given two \(B\) matrices representing two "input wires" to an operation - either addition, multiplication or negation - you can generate a \(B\) matrix representing the output wire. Given valid encodings for two inputs to an operation, you can get an encoding for the output - \(1 - m\), \(m_a + m_b\) or \(m_a * m_b\) - that is based on the \(B\) matrix representing the output wire. Importantly, this is not fully-homomorphic encryption: to multiply, you need to know either \(m_a\) or \(m_b\) in the clear.

In BGG+14, there is a step at the end, which allows decoding a pre-selected output, only if the value in the computation on some "output" wire equals 0. Here, we do not do that. Instead, what we will do is just extract the data that we need from the encoding. But in both cases, the decryption method depends on the encoding being based on a specific matrix \(B_{final}\) representing the output wire - this is how we enforce that decryption can happen if you did the computation correctly, but not in any other context.

The core of diamond iO is:

The complexity of diamond iO rests in three places:

The first two ideas came from prior work, particularly HLL23 and AKY24. The third piece, the mechanism for constructing the ABE encodings of the inputs, originally came from GGH15; the new contribution in diamond iO is to use it not to evaluate the whole program (which turned out insecure) but to generate BGG+ encodings for the inputs.

We will tackle these three pieces in turn.

The decryption step

Assume for now that the evaluator somehow gets as input four types of BGG+ encodings of:

We'll label this ensemble \([1, E[z], x, t]\).

The evaluator first runs the BGG+ computation using the encodings of \(E[z]\) and \(x\). Remember, the computation is not \(f(x, z)\) directly, rather, it's \(FHE.eval(f, x, E[z])\). From the BGG+ perspective, \(x\) and \(E[z]\) are both cleartext bits. From the FHE perspective, \(z\) is a hidden input, of which only the encrypted form is known to the evaluator.

At the end of doing the BGG+ computation, the evaluator has BGG+ encodings of bits of the FHE ciphertext representing \(f(x, z)\).

BGG+ is not fully homomorphic encryption; in general, computing on BGG+ encodings requires having the underlying cleartext. But we can avoid this rule for additions, and for multiplications by known values. This is because a BGG+ encoding of \(m_a * m_b\) is computed via:

\(c_{out} = m_b * ​c_a ​+ c_b * ​G^{-1}(B_a)\)

(If the encodings were adding \(G * m\) instead of subtracting, then it would be \(G^{-1}(-B_a)\) instead)

We can allow \(m_a\) to be unknown (ie. given to us via encodings only), as long as \(m_b\) is known.

This is an important fact for us. To see why, remember the structure of GSW decryption (also described in the previous post in this series):



Because the evaluator knows all the bits of the actual execution trace, including the final FHE ciphertext (which we'll call \(Y\)), we can give the evaluator the FHE decryption key, \(t\), only as BGG+ encodings.

Let's see how the decryption works.

We want to compute \(t * Y\). We have only BGG+ encodings of \(t\), and we have both the BGG+ encodings and the raw bits of \(Y\). So in principle we can do it.

But there's a problem: \(Y\) is given to us as a series of bits. That is, we don't get a vector of the form \([2, 6, 11...]\). We get a vector of the form \([0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1...]\), where each bucket of bits (in this example, 4 bits) is a binary-encoding of a number, by convention least-significant-bits first. So we have to somehow re-scale the bits, so that in every cell where we BGG+ multiply some cell \(t_j * Y_{\{i,j,b\}}\), where \(Y_{\{i,j,b\}}\) is the order \(2^b\) bit of the cell \(Y_{\{i,j\}}\), the encoding that comes out is scaled by \(2^b\).

For convenience, let's group together encodings of bits that are of the same order and that will eventually be inside the same value in the answer: that is, the whole column \(t_j * Y_{\{i,j,b\}}\) for some specific \(i\) and \(b\) across all \(j\).

\(\sum_j s * (B_{\{out,i,j,b\}} - t_j * Y_{\{i,j,b\}} * G) + e_{\{out,i,j,b\}}\)

\(= s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}\)



Now we get to the puzzle: how do we combine together bits of different orders?

One naive thing we could do is just rescale and add:

\(\sum_{b=0}^{log(q)-1} (s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}) * 2^b\)

The problem with this is that it multiplies up the errors of the high-order digits too much: the error of the highest-order term would get multiplied by \(2^{log(q)-1} = \frac{q}{2}\) so it would flood the whole range.

So here's what we do instead:

\(\sum_{b=0}^{log(q)-1} (s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}) * G^{-1}(2^bG)\)

\(G^{-1}(2^bG)\) is doing all the work here. Basically, this is a bucket-wise left shift operator - it takes each \(log(q)\)-bit bucket \(tY\), and shifts them all to the left.

Remember, BGG+ encodings are of the form \(s * (B - G * m) + e\). And remember that \(G\) has this form:



This means that BGG+ encodings encode \(m\) simultaneously at every scale. And what we're doing here is we're moving higher-scale encodings of higher-order bits into the first column of each bucket. And because we're just doing additions and shifts, not multiplications, we avoid the error blowing up by more than the small amount that is introduced by the additions.

To see why \(G^{-1}(2^bG)\) has this shift effect, let's also look at \(G^{-1}\). On its own, \(G^{-1}\) is the right-inverse of \(G\), the bit decomposition operator (not a matrix).

Hence, plain \(G^{-1}(G)\) is just the identity matrix:



But if we multiply \(G\) by \(2\) before sticking it into \(G^{-1}\), each binary decomposition is shifted up by one place, and so we get the desired shift:



This is just a notational trick that allows the "left-shift every bucket" mechanism to be easily encoded into vector-and-matrix algebra. See also that the \(G^{-1}(2^bG)\) matrix is also clearly very low-norm (only ones and zeroes); this is another way to know that this step does not unreasonably blow up error.

The final outcome of this step is that you get a BGG+ encoding of \(tY\):

\(s * (B_{\{final,i\}} - G * (tY)_i) + e_{\{final,i\}}\)

Note that these are no longer "normal" encodings, both because the \((tY)_i\)'s are full-range values modulo q, and because the evaluator does not know the cleartext values. The evaluator cannot do any further BGG+ multiplication on them. But that's okay, because they do not have to; the only step the evaluator has left is to remove the "wrapping with \(s\)" so that we can get to our final goal, extracting \((tY)_i + e\).

To extract \((tY)_i + e\), we are going to diverge from BGG+ ABE fully. In ABE, we know that, in those cases where decryption is supposed to be possible, the answer \(C(tag)\) equals 0. \(G * C(tag)\) disappears, and that's exactly why we can extract the message. \(G * C(tag)\) plays the role of a blinding factor. Here, the use case is different: \((tY)_i\) is the entire thing that we want to extract.

Let's rewrite our BGG+ encoding as:

\(s * B_{\{final,i\}} - s * G * (tY)_i + e_{\{final,i\}}\)

Now, let's extract a single column from this expression - the lowest-order column in the last bucket.



\(s * b_i - s * g * (tY)_i + e\)

The choice of lowest-order column diverges from "traditional GSW decryption", where we extract the highest-order column, but remember: because of our bucket-wise shifting trick, by this point the lowest-order column contains information that was grabbed from the highest-order columns representing the values that we care most about (the message).

We constrain \(s\) so that its last value (which we'll denote \(s_{last}\)) equals -1. Also, notice that the column of \(G\) we chose, \(g\), is just a bunch of zeroes followed by a 1. Hence, we get a nicely simplified expression:

\(s * b_i - s_{last} * (tY)_i + e_i\)

\(= s * b_i + (tY)_i + e_i\)

Now, our task is to compute \(s * b_i\) so we can remove it.

Let us pull in one fact from a later section: the evaluator has "access" to \(s\) in the form of an encoded sample \(ct = (m \otimes s) P_L + e_L\), where \(m\) is the full input to the obfuscated computation and \(\otimes\) is the "tensor product". That is, \(m \otimes s\) is a vector of length \(len(m) * len(s)\) that contains \(s\) multiplied by the first value in \(m\), then right after that \(s\) multiplied by the second value in \(m\), and so on. Conveniently, the first value in \(m\) is fixed to be 1.



Now, we use the trapdoor machinery, described in the ABE section of the previous post, to make a low-norm preimage \(K_G\) that satisfies:

\(P_L * K_G = M\)

Here, \(M\) is a matrix that contains all of the \(b_i\)'s - that is, the extracted columns from each \(B_{\{final,i\}}\) matrix - and then is padded with extra zeroes to make the dimensions match up.

This ensures that \(ct * K_G\) evaluates to

\(((m \otimes s) * P_L + e_L) * K_G\)

\(= (m \otimes s) * P_L * K_G + e_L * K_G\)

\(= (m \otimes s) * M + e_g\)

Now, notice that only the first \(len(s)\) rows of \(M\) are nonzero, and the first \(len(s)\) values in \(m \otimes s\) are just \(s\). So the above reduces to:

\(s * M + e_g\)

Which is a concatenation of all of the \(s * b_i + e_{\{i,g\}}\) that we need.

Now, we can subtract that, and get the \((tY)_i + e_i\) values we need, which are our FHE decryption result. And then finally, the evaluator rounds, to get the final value of \(f(x, z)\).

Here we can once again see where the security comes from: the \(K_G\) "trapdoor" allows the evaluator to extract this only for those specific \(b_i\), and the \(b_i\) come from \(B_{\{final,i\}}\), which are bound to the exact shape of the circuit. If the evaluator computes \(s * b'_i + e{\{i,g\}}\) for some different \(b'_i\), then they would be left with a \(s * (b_i - b'_i)\) term blinding the \((tY)_i + e_i\), and would not be able to extract the answer.

Adjustments to f(x, z)

It turns out the security of the scheme we described above is only provable under one condition: that the outputs \((tY)_i + e_i\) are uniformly random - in fact, jointly pseudorandom across all \(2^L\) inputs. "Jointly pseudorandom" here basically means that if you were given unlimited access to the full exponentially-sized table (though still only polynomial computation time), you would not be able to distinguish it from random data.

This actually makes our work challenging. In a "normal" GSW decryption, the last entry of \(t * Y\) equals \(\frac{q}{2} * m + e\). This would give us two problems:

  1. Because the error is almost always much smaller than the message, the output is very "spiky" around \(0\) and \(\frac{q}{2}\). Even if the error could somehow always cover the full range (difficult: worst-case and average-case tend to differ massively), LWE error is correlated with the message so it would still leak information
  2. The distribution of outputs \(m\) is itself not uniform, because most "natural" functions \(f\) are not random.

We can tackle these in turn.

First, we modify the function that we evaluate in FHE. We make \(z\) contain two secrets \((z_1, z_2)\): the secret input to the function, and a random PRF key. Then, we do \(FHE.eval(f', x, E[z])\), where:

\(f'(x, z) = \frac{q}{2} * f(x, z_1) - (\frac{q}{4} - e_{max}) + H_1(x, z_2)\)

Here, \(H_1\) is a hash function that returns values in \([0 ... \frac{q}{2}-2e_{max})\), where \(e_{max}\) is a bound on the largest possible error that our whole procedure could introduce. Formally, \(H_1\) must be a PRF ("pseudorandom function").

For correctness, it's okay to just think of it as a hash. However, in practice, computing this hash is where almost all of the evaluation time lies, and so making diamond iO practical will almost certainly involve heavily optimizing \(H_1\) to target its specific needs, and not using generic algorithms like SHA256.

In "normal" GSW, the \(\frac{q}{2}\) factor is implicit - of course you multiply the message by \(\frac{q}{2}\) because otherwise it might get smudged by the error. Here, we multiply the \(f(x, z_1)\) part by \(\frac{q}{2}\) but not the \(H_1(x, z_2)\) part, and we're okay with a few lower-order digits of that getting mangled by the error - the \(H_1(x, z_2)\) is not data we care about recovering, rather it's meant to be "maximum-range" pseudorandom noise that is there to smudge out the FHE and ABE-related noise. The goal of the \(\frac{q}{4} - e_{max}\) offset is to "center" the output of \(H_1\) (ie. make its range \([-(\frac{q}{4} - e_{max}), \frac{q}{4} - e_{max}]\)), so that you can round to the nearest \(\frac{q}{2}\) and then divide to get back \(f(x, z)\).

Second, we modify the function again, by xoring a hash into \(f\) itself:

\(f'(x, z) = \frac{q}{2} * xor(f(x, z_1), H_2(x)) - (\frac{q}{4} - e_{max}) + H_1(x, z_2)\)

From \(H_2\), we want a random oracle property. But \(H_2\) is not a bottleneck for efficiency, so using generic algorithms like SHA256 is great here.

To get the final output in the clear, we xor \(H_2(x)\) back out after we get the final \(f'(x, z)\) output from the diamond iO.

The result of this trickery is that \((tY)_i\) is now fully pseudorandom:



This technique is sometimes called "PROM bootstrap", and it is a generic transformation that converts "obfuscation for pseudorandom functionalities" into a more general form of obfuscation that works for any \(f\). Looking at a hash simultaneously as a random oracle and an implementable circuit is also sometimes considered a "sketchy" cryptographic assumption, though it is used widely today, eg. in recursive STARKs.

Finally, if we want to do "traditional" iO, which hides the function, instead of fixing a public function \(f\) and hiding a private input \(z\), then we do the trick we already did many times in the more conservative forms of obfuscation: make \(f\) be a "universal circuit" \(VM\) whose private input is a program \(P\), and that satisfies \(VM(x, P) = P(x)\).

How does the evaluator learn the input encodings?

So far, we have just magically assumed that the evaluator gets BGG+ encodings \(s * (B_i - G * m_i) + e_i\). But unfortunately things are not that simple. The obfuscator could construct \(len(m) + L\) such encodings, for \(m_i \in \{0,1\}\), with the same \(s\) (they would only have to provide both the \(0\) option and the \(1\) option for the \(x\) portion of \(m\)). But this is not secure: if you get two encodings with the same \(B_i\) and the same \(s\) but different \(m_i\) (if we're dealing with binary bits, that means \(0\) and \(1\)), then you can just subtract them and get \(s * G + (e_1 - e_2)\), leaking \(s\) in the clear.

So we have to somehow give the evaluator a gadget that lets them produce encodings for any \(m\), but with a different \(s\) for each \(m\). Even one bit of change in \(m\) should completely change the \(s\). This is where the core of diamond iO's machinery comes in.

The trick is as follows. First, remember the structure of \(m\): \([1, E[z], x, t]\). The only thing that changes between different evaluations is \(x\).

We will start off with a "base" message, \(m_0 = [1, E[z], 000...000, t]\), and a "base" secret \(s_0\).

For the i'th bit, we will define matrices \(M_{\{i,0\}}\) and \(M_{\{i,1\}}\) which are the identity matrix, but with the 1 in the slot corresponding to the i'th bit of \(x\) either deleted or moved to the first row. That is, \(M_{\{i,b_i\}}\) represents the idea "keep \(m\) as is, except change the i'th bit in \(x\) to \(b_i\)".



We also define low-norm matrices \(S_{\{i,0\}}\) and \(S_{\{i,1\}}\), whose goal is to transform the secret.

Hence, any final vector \(m_L\) can be expressed as \(m_0 * M_{\{1,b_1\}} * M_{\{2,b_2\}} * ... * M_{\{L,b_L\}}\). And the corresponding \(s_0 * S_{{1,b_1}} * S_{{2,b_2}} * ... * S_{{L,b_L}}\) gives us a unique secret \(s_L\). Our goal will be to "fuse" these transformations together, so that for each specific \(m_L\) you generate, you get BGG+ encodings constructed with a unique corresponding secret \(s_L\).

We will "fuse" these vectors and matrices by tensoring them together, and then wrapping them in encodings so that they cannot be unpacked.

A mini example - we'll start with tensoring the vectors \(m\) and \(s\):



The leftmost value in \(m\) is constrained to equal \(1\), and the rightmost value in \(s\) is constrained to equal \(-1\).

And now this is what tensoring matrices looks like (all un-written entries are zeroes):



See how \(M\) is in the shape we described above, and \(S\) is constrained so that it doesn't touch the last column, keeping the last value of \(s\) at -1.

A key property of tensoring is that if \(m_1 = m_0 * M_{\{1, b_1\}}\) and \(s_1 = s_0 * S_{\{1, b_1\}}\), then \((m_1 \otimes s_1) = (m_0 \otimes s_0) * (M_{\{1, b_1\}} \otimes S_{\{1, b_1\}})\). For convenience, we'll define \(Q_{\{i,b\}}\) as equaling \((M_{\{i, b\}} \otimes S_{\{i, b\}})\)

So far, the evaluator's formula is: start with \(q_0 = m_0 \otimes s_0\), multiply by each \(Q_{\{i, b_i\}} =M_{\{i, b_i\}} \otimes S_{\{i, b_i\}}\) in sequence, and then get \(q_L = m_L \otimes s_L\) at the end.

We have already gotten somewhere: we now have a way to generate some kind of encoding of each possible \(m_L\) that the evaluator might need, such that it's bound to a unique \(s_L\). The problem: so far, this encoding exposes \(m_L\) (including the \(t\) bits which the evaluator is not meant to know) and \(s_L\) completely in the clear.

The next step will be multiplying in matrices in the right places to "blind" these encodings at each step.

The obfuscator will give the evaluator an encoding \(p_0 ​=q_0​ * P_0 ​+ e_0\)

The obfuscator will then generate low-norm matrices \(K_{\{i,b\}}\), which satisfy \(P_{i-1} * K_{\{i,b\}} = Q_{\{i,b\}} * P_i + E_{\{i,b\}}\). This is allowed, again, because of the trapdoor mechanism.

The evaluator can then walk down the exponentially-sized tree of possible encodings. Here's what this tree looks like graphically. Notice that even though the tree is of size \(2^L\), the number of actually-distinct "edge matrices" that generate this whole tree is only \(2L+1\), including the final \(K_f\) that we'll get to later).



And here's why this works mathematically:

\(p_1 = p_0 * K_{\{1,b_1\}}\)

\(= q_0 * P_0 * K_{\{1,b_1\}} + e_0 * K_{\{1,b_1\}}\)

\(= q_0 * Q_{\{1,b_1\}} * P_1 + e_0 * K_{\{1,b_1\}} + q_0 * E_{\{1,b_1\}}\)

\(= q_0 * Q_{\{1,b_1\}} * P_1 + e_1\)

And then:

\(p_2 = p_1 * K_{\{2,b_2\}}\)

\(= q_0 * Q_{\{1,b_1\}} * P_1 * K_{\{2,b_2\}} + e_1 * K_{\{2,b_2\}}\)

\(= q_0 * Q_{\{1,b_1\}} * Q_{\{2,b_2\}} * P_2 + e_1 * K_{\{2,b_2\}} + q_0 * Q_{\{1,b_1\}} * E_{\{2,b_2\}}\)

\(= q_0 * Q_{\{1,b_1\}} * Q_{{2,b_2}} * P_2 + e_2\)

And so on all the way up until \(p_L = q_0 * Q_{\{1,b_1\}} * ... * Q_{\{L,b_L\}} * P_L + e_L\), where \(q_0 * Q_{{1,b_1}} * ... * Q_{{L,b_L}} = q_L\).

Notice \(e_{i-1} * K_{\{i,b_i\}}\) and \(q_{i-1} * E_{{i,b_i}}\) both collapse into \(e_i\). This means that \(K_{\{i,b\}}\), \(Q_{\{i,b\}}\) and \(q\) (and hence \(m\), \(s\), \(M_{\{i,b\}}\), \(S_{\{i,b\}}\)) all have to be low-norm (and they are).

Now, we have an actually-blinded encoding of \(q_L = m_L \otimes s_L\). But it's not quite the right format. We have:

\((m_L \otimes s_L) * P_L + e_L\)

We need:

\(s_L * (B_i - G * m_L[i]) + e_i\)

To cross this bridge, we add another trapdoor \(K_f\). It needs to satisfy:

\(P_L * K_f = u_1 \otimes B_{full} - I \otimes G\)

Here we just introduced a few pieces of new notation, so let's go through them:

Also notice that here (like the \(K_G\) case, unlike the \(K_{\{i,b\}}\) case), there is no error on the right side; that is fine, because the error in the final ciphertexts will come from elsewhere, and the value on the right is a public expression, so there is no security risk to not masking it.

Now, let's see what happens if we multiply \((m_L \otimes s_L) * P_L + e_L\) by \(K_f\):

\(((m_L \otimes s_L) * P_L + e_L) * K_f\)

\(= (m_L \otimes s_L) * P_L * K_f + e_L * K_f\)

\(= (m_L \otimes s_L) * u_1 \otimes B_{full} - (m_L \otimes s_L) * (I \otimes G) + e_f\)

\(= s_L * B_{full} - (m_L \otimes s_L) * (I \otimes G) + e_f\)

\(= s_L * B_{full} - m_L \otimes (s_L * G) + e_f\)

The last two lines are the most nonobvious. What's going on is: because \(m_L\) (like all message vectors) starts with 1, the first bucket of \(m_L \otimes s_L\) is just \(s_L\). And on the other hand, below the first bucket of rows, \(u_1 \otimes B_{full}\) just equals zero. Hence, \((m_L \otimes s_L) * (u_1 \otimes B_{full})\) just reduces to \(s_L * B_{full}\). And then the \(I \otimes G\) plays its role as many copies of \(G\) that all get multiplied by many copies of \(s\), one for each message value in \(m_L\).

(In the paper, they split up \(B_{full}\) into \(B_{att}\) (excluding \(t\) in the inputs) and \(B_t\), and run the above procedure on those two pieces separately. This is mathematically equivalent to what was described here. The conceptual reason to separate is that they are different kinds of data - the raw values of \([1, E[z], x]\) are given to the evaluator, but the raw values in \(t\) are not)



And the output is literally just the concatenation of all the \(s_L * (B_i - G * m_L[i]) + e_{\{f,i\}}\) BGG+ encodings that we need.

Re-summarizing diamond iO

Security

The most "controversial" part of this protocol is the mechanism for generating the BGG+ encodings for the input. This relies on two fairly new cryptographic assumptions, called all-product LWE and evasive LWE. Evasive LWE was introduced in Wee22; all-product LWE is unique to diamond iO.

These LWE assumptions are at the same time plausible, avoiding many pitfalls that we have come to understand from a decade of attempts to create novel LWE assumptions that ended up broken by "zeroizing" attacks, but also novel and risky. Evasive LWE is non-falsifiable in Naor's sense: formally, it says "for every algorithm that can distinguish X there exists an algorithm that can distinguish Y..." so it's hard to tell if you even have a counterexample. There are already known classes of situations in which evasive LWE is false; diamond iO deliberately chooses a type of evasive LWE assumption that avoids this.

More security analysis is needed to verify that these assumptions are safe.

Efficiency

Constructing the BGG+ encodings is relatively "easy" (in part because you can actually use a fan-out greater than 2, eg. a fan-out of 256 fills in 8 bits of \(x\) per multiplication). The hard part is the BGG+ evaluation, which has "ABE * FHE" overhead, where the FHE is itself GSW, which nobody uses in production because it's far less efficient than more mainstream algorithms like BFV. GSW is needed because it has "nice" algebraic properties, including no need for relinearization or key-switching, and the ability to put plaintext into any size bits of the ciphertext.

On top of this, there are other sources of inefficiency and limitation:

The main concrete bottleneck is actually computation of \(H_1(x, z)\) inside of the ABE * FHE. The main contributor to cost is depth (as depth increases the dimensions needed to handle the error). If the computation itself is deep, there are ways to "refresh" noise for BGG+ encodings, but these methods themselves depend on having a feasible \(H_1\). Hence, depth of \(H_1\) is the concrete bottleneck. Diamond iO currently uses a "tree" of Goldreich PRG executions to instantiate \(H_1\). This has multiplicative depth 3 per fan-out, and the number of fan-outs required is proportional to the input length.

These reasons are why, in the chart showing different types of obfuscation at the start of this post, diamond iO lands in the "very aggressive" and "planetary" point of the chart.

If we want to improve concrete efficiency of diamond iO, a few possible paths come to mind:

A key advantage of diamond iO is its friendliness to analysis, because of its simplicity. There are no towers of "protocols inside protocols inside protocols" like in more conservative obfuscation variants - the only nesting is the FHE inside of ABE. It can be understood without reference to the entire tower of constructions invented over the past 20 years by cryptographers: you don't need sublinear randomized encodings, functional encryption, garbled circuits, split FHE or even full ABE (we're using the core machinery of BGG+, but the details are sufficiently different that the advantage to someone who already knows how it works is much lower). For these reasons, I hope that diamond iO can get much more attention and analysis on both security and optimization, and we can get to an obfuscation protocol that we can actually run.