In this post I’m going to show how you turn a truth table into a digital logic circuit that uses XOR and AND gates.
My Usage Case
My specific usage case for this is in my investigations into homomorphic encryption, which as you may recall is able to perform computation on encrypted data. This lets encrypted data be operated on by an untrusted source, given back to you, and then you can decrypt your data to get a result.
Lots of use cases if this can ever get fast enough to become practical, such as doing cloud computing with private data. However, when doing homomorphic encryption (at least currently, for the techniques I’m using), you only have XOR and AND logic operations.
So, I’m using the information in this post to be able to turn a lookup table, or a specific boolean function, into a logic circuit that I can feed into a homomorphic encryption based digital circuit.
Essentially I want to figure out how to do a homomorphic table lookup to try and make some simple as possible circuits, that will in turn be as fast and lean as possible.
If you want to know more about homomorphic encryption, here’s a post I wrote which explains a very simple algorithm: Super Simple Symmetric Leveled Homomorphic Encryption Implementation
Algebraic Normal Form
Algebraic normal form (ANF) is a way of writing a boolean function using only XOR and AND.
Since it’s a normal form, two functions that do the same thing will be the same thing in ANF.
There are other forms for writing boolean logic, but ANF suits me best for my homomorphic encryption circuit needs!
An example of boolean logic in ANF is the below:
It is essentially a boolean polynomial, where AND is like multiplication, and XOR is like addition. It even factors the same way. In fact, ANF is not always the smallest circuit possible, you’d have to factor common ANDs to find the smallest way you could represent the circuit, like the below:
That smaller form does 1 AND and 2 XORs, versus the ANF which does 3 ANDs and 2 XORs. In homomorphic encryption, since AND is so much more costly than XOR, minimizing the ANDs is a very nice win, and worth the effort.
Wikipedia has some more info about ANF here: Wikipedia: Algebraic normal form
Truth Tables and Lookup Tables
A truth table is just where you specify the inputs into a boolean function and the output of that boolean function for the given input:
A lookup table is similar in functionality, except that it has multi bit output. When dealing with digital circuits, you can make a lookup table by making a truth table per output bit. For instance, the above truth table might just be the low bit of the lookup table below, which is just a truth table for addition of the input bits.
Converting Truth Table to ANF
When I first saw the explanation for converting a truth table to ANF, it looked pretty complicated, but luckily it turns out to be pretty easy.
The basic idea is that you make a term for each possible combination of x inputs, ANDing a term by each constant, and then solving for those constants.
Let’s use the truth table from the last section:
For three inputs, the starting equation looks like this:
Now we have to solve for the a values.
To solve for , we just look in the truth table for function
to see if we have an odd or even number of ones in the output of the function. If there is an even number, it is 0, else it is a 1.
Since we have an even number of ones, the value is 0, so our equation becomes this:
Note that is the symbol for AND. I’m showing it explicitly because otherwise the equation looks weird, and a multiplication symbol isn’t correct.
Since 0 ANDed with anything else is 0, and also since n XOR 0 = n, that whole last term disappears, leaving us with this equation:
Next up, to solve for , we need to limit our truth table to
. That truth table is below, made from the original truth table, but throwing out any row where
is 1.
We again just look at whether there are an odd or even number of ones in the function output, and use that to set appropriately. In this case, there are an even number, so we set it to 0, which makes that term disappear again. Our function is now down to this:
If we look at , we find that it also has an even number of ones, making
become 0 and making that term disappear.
Looking at , it also has an even number of ones, making
become 0 and making that term disappear as well.
That leaves us with this equation:
To solve for , we look at the truth table for
, which is below:
There are an odd number of ones in the output, so becomes 1. Finally, we get to keep a term! The equation is below:
Since 1 AND n = n, we can drop the explicit 1 to become this:
If you do the same process for and
, you’ll find that they also have odd numbers of ones in the output so also become ones. That puts our equation at:
Solving for , is just looking at whether there are an odd or even number of ones in the function
which you can look up directly in the lookup table. It’s even, so
becomes 0, which makes our full final equation into this:
We are done! This truth table can be implemented with 3 XORs and 0 ANDs. A pretty efficient operation!
You can see this is true if you work it out with the truth table. Try it out and see!
Sample Code
Here is some sample code that lets you define a lookup table by implementing an integer function, and it generates the ANF for each output bit for the truth table. It also verifies that the ANF gives the correct answer. It shows you how to use this to make various circuits: bit count, addition, multiplication, division and modulus.
#include <stdio.h> #include <array> #include <vector> #define PRINT_TRUTHTABLES() 0 #define PRINT_NUMOPS() 1 #define PRINT_ANF() 1 void WaitForEnter () { printf("Press Enter to quit"); fflush(stdin); getchar(); } template <size_t NUM_INPUT_BITS> bool LookupEntryPassesMask (size_t entry, size_t mask) { for (size_t i = 0; i < NUM_INPUT_BITS; ++i) { const size_t bitMask = 1 << i; const bool allowOnes = (mask & bitMask) != 0; const bool bitPassesMask = allowOnes || (entry & bitMask) == 0; if (!bitPassesMask) return false; } return true; } template <size_t NUM_INPUT_BITS> bool ANFHasTerm (const std::array<size_t, 1 << NUM_INPUT_BITS> &lookupTable, size_t outputBitIndex, size_t termMask) { const size_t c_inputValueCount = 1 << NUM_INPUT_BITS; int onesCount = 0; for (size_t i = 0; i < c_inputValueCount; ++i) { if (LookupEntryPassesMask<NUM_INPUT_BITS>(i, termMask) && ((lookupTable[i] >> outputBitIndex) & 1) != 0) onesCount++; } return (onesCount & 1) != 0; } template <size_t NUM_INPUT_BITS> void MakeANFTruthTable (const std::array<size_t, 1 << NUM_INPUT_BITS> &lookupTable, std::array<size_t, 1 << NUM_INPUT_BITS> &reconstructedLookupTable, size_t outputBitIndex) { const size_t c_inputValueCount = 1 << NUM_INPUT_BITS; printf("-----Output Bit %u-----rn", outputBitIndex); // print truth table if we should #if PRINT_TRUTHTABLES() for (size_t inputValue = 0; inputValue < c_inputValueCount; ++inputValue) printf(" [%u] = %urn", inputValue, ((lookupTable[inputValue] >> outputBitIndex) & 1) ? 1 : 0); printf("rn"); #endif // find each ANF term std::vector<size_t> terms; for (size_t termMask = 0; termMask < c_inputValueCount; ++termMask) { if (ANFHasTerm<NUM_INPUT_BITS>(lookupTable, outputBitIndex, termMask)) terms.push_back(termMask); } // print function params #if PRINT_ANF() printf("f("); for (size_t i = 0; i < NUM_INPUT_BITS; ++i) { if (i > 0) printf(","); printf("x%i",i+1); } printf(") = rn"); #endif // print ANF and count XORs and ANDs size_t numXor = 0; size_t numAnd = 0; if (terms.size() == 0) { #if PRINT_ANF() printf("0rn"); #endif } else { for (size_t termIndex = 0, termCount = terms.size(); termIndex < termCount; ++termIndex) { if (termIndex > 0) { #if PRINT_ANF() printf("XOR "); #endif ++numXor; } size_t term = terms[termIndex]; if (term == 0) { #if PRINT_ANF() printf("1"); #endif } else { bool firstProduct = true; for (size_t bitIndex = 0; bitIndex < NUM_INPUT_BITS; ++bitIndex) { const size_t bitMask = 1 << bitIndex; if ((term & bitMask) != 0) { #if PRINT_ANF() printf("x%i ", bitIndex + 1); #endif if (firstProduct) firstProduct = false; else ++numAnd; } } } #if PRINT_ANF() printf("rn"); #endif } } #if PRINT_ANF() printf("rn"); #endif #if PRINT_NUMOPS() printf("%u XORs, %u ANDsrnrn", numXor, numAnd); #endif // reconstruct a bit of the reconstructedLookupTable for each entry to be able to verify correctness const size_t c_outputBitMask = 1 << outputBitIndex; for (size_t valueIndex = 0; valueIndex < c_inputValueCount; ++valueIndex) { bool xorSum = false; for (size_t termIndex = 0, termCount = terms.size(); termIndex < termCount; ++termIndex) { size_t term = terms[termIndex]; if (term == 0) { xorSum = 1 ^ xorSum; } else { bool andProduct = true; for (size_t bitIndex = 0; bitIndex < NUM_INPUT_BITS; ++bitIndex) { const size_t bitMask = 1 << bitIndex; if ((term & bitMask) != 0) { if ((valueIndex & bitMask) == 0) andProduct = false; } } xorSum = andProduct ^ xorSum; } } if (xorSum) reconstructedLookupTable[valueIndex] |= c_outputBitMask; } } template <size_t NUM_INPUT_BITS, size_t NUM_OUTPUT_BITS, typename LAMBDA> void MakeANFLookupTable (const LAMBDA& lambda) { // make lookup table const size_t c_outputValueMask = (1 << NUM_OUTPUT_BITS) - 1; const size_t c_inputValueCount = 1 << NUM_INPUT_BITS; std::array<size_t, c_inputValueCount> lookupTable; for (size_t inputValue = 0; inputValue < c_inputValueCount; ++inputValue) lookupTable[inputValue] = lambda(inputValue, NUM_INPUT_BITS, NUM_OUTPUT_BITS) & c_outputValueMask; // make the anf for each truth table (each output bit of the lookup table) std::array<size_t, c_inputValueCount> reconstructedLookupTable; std::fill(reconstructedLookupTable.begin(), reconstructedLookupTable.end(), 0); for (size_t outputBitIndex = 0; outputBitIndex < NUM_OUTPUT_BITS; ++outputBitIndex) MakeANFTruthTable<NUM_INPUT_BITS>(lookupTable, reconstructedLookupTable, outputBitIndex); // verify that our anf expressions perfectly re-create the lookup table for (size_t inputValue = 0; inputValue < c_inputValueCount; ++inputValue) { if (lookupTable[inputValue] != reconstructedLookupTable[inputValue]) printf("ERROR: expression / lookup mismatch for index %urn", inputValue); } printf("expression / lookup verification complete.rnrn"); } size_t CountBits (size_t inputValue, size_t numInputBits, size_t numOutputBits) { // Count how many bits there are int result = 0; while (inputValue) { if (inputValue & 1) result++; inputValue = inputValue >> 1; } return result; } size_t AddBits (size_t inputValue, size_t numInputBits, size_t numOutputBits) { // break the input bits in half and add them const size_t bitsA = numInputBits / 2; const size_t mask = (1 << bitsA) - 1; size_t a = inputValue & mask; size_t b = inputValue >> bitsA; return a+b; } size_t MultiplyBits (size_t inputValue, size_t numInputBits, size_t numOutputBits) { // break the input bits in half and add them const size_t bitsA = numInputBits / 2; const size_t mask = (1 << bitsA) - 1; size_t a = inputValue & mask; size_t b = inputValue >> bitsA; return a * b; } size_t DivideBits (size_t inputValue, size_t numInputBits, size_t numOutputBits) { // break the input bits in half and add them const size_t bitsA = numInputBits / 2; const size_t mask = (1 << bitsA) - 1; size_t a = inputValue & mask; size_t b = inputValue >> bitsA; // workaround for divide by zero if (b == 0) return 0; return a / b; } size_t ModulusBits (size_t inputValue, size_t numInputBits, size_t numOutputBits) { // break the input bits in half and add them const size_t bitsA = numInputBits / 2; const size_t mask = (1 << bitsA) - 1; size_t a = inputValue & mask; size_t b = inputValue >> bitsA; // workaround for divide by zero if (b == 0) return 0; return a % b; } int main (int argc, char **argv) { //MakeANFLookupTable<3, 2>(CountBits); // Output bits needs to be enough to store the number "input bits" //MakeANFLookupTable<4, 3>(AddBits); // Output bits needs to be (InputBits / 2)+1 //MakeANFLookupTable<4, 4>(MultiplyBits); // Output bits needs to be same as input bits //MakeANFLookupTable<4, 2>(DivideBits); // Output bits needs to be half of input bits (rounded down) //MakeANFLookupTable<4, 2>(ModulusBits); // Output bits needs to be half of input bits (rounded down) //MakeANFLookupTable<10, 5>(DivideBits); // 5 bit vs 5 bit division is amazingly complex! MakeANFLookupTable<4, 2>(ModulusBits); // Output bits needs to be half of input bits (rounded down) WaitForEnter(); return 0; }
Sample Code Runs
Here is the program output for a “bit count” circuit. It counts the number of bits that are 1, in the 3 bit input, and outputs the answer as 2 bit output. Note that the bit 0 output is the same functionality as the example we worked through by hand, and you can see that it comes up with the same answer.
Here is the program output for an adder circuit. It adds two 2 bit numbers, and outputs a 3 bit output.
Here is the program output for a multiplication circuit. It multiplies two 2 bit numbers, and outputs a 4 bit number.
Here is the program output for a division circuit. It divides a 2 bit number by another 2 bit number and outputs a 2 bit number. When higher bit counts are involved, the division circuit gets super complicated, it’s really crazy! 5 bit divided by 5 bit is several pages of output for instance. Note that it returns 0 whenever it would divide by 0.
Lastly, here is the program output for a modulus circuit. It divides a 2 bit number by another 2 bit number and outputs the remainder as a 2 bit number.
Closing and Links
While the above shows you how to turn a single bit truth table into ANF, extending this to a multi bit lookup table is super simple; you just do the same process for each output bit in the lookup table.
Here are a few links in case anything above is unclear, or you want more information.
Finding Boolean/Logical Expressions for truth tables in algebraic normal form(ANF)