fhEVM v0.2.0: New Operators, Simpler Syntax and Optimized Performances

October 17, 2023
Levent Demir

Since the release of Zama's fhEVM white paper and alpha code a few weeks ago, the Zama team has released many minor versions. The latest to date (v0.2.0) introduces a comprehensive set of operators designed to provide robust functionality and versatility.

Supported types

The following encrypted data types are now supported:

  • [.c-inline-code]ebool[.c-inline-code]
  • [.c-inline-code]euint8[.c-inline-code]
  • [.c-inline-code]euint16[.c-inline-code]
  • [.c-inline-code]euint32[.c-inline-code]

The equivalent signed types (and 64 bits) are coming soon. 

Arithmetic operators

Some operators are overloaded with the usual symbol, below is an example for addition overload:

# the transfer method received amount (euint32) 
balances[to] = balances[to] + amount;
# instead of
# balances[to] = TFHE.add(balances[to], amount);
  • Addition ([.c-inline-code]add[.c-inline-code])
  • Subtraction ([.c-inline-code]sub[.c-inline-code])
  • Multiplication ([.c-inline-code]mul[.c-inline-code])
  • Division ([.c-inline-code]div[.c-inline-code]) (only with plaintext divisor)
  • Remain ([.c-inline-code]rem[.c-inline-code]) (only with plaintext divisor)

Bitwise operators

The bitwise operations do not natively accept a mix of ciphertext and plaintext inputs, but the TFHE library adds function overloads to handle those cases.

// a & b
function and(euint8 a, euint8 b) internal view returns (euint8)

// implicit trivial encryption of `b` before calling the operator
function and(euint8 a, uint16 b) internal view returns (euint16)
  • Bitwise AND ([.c-inline-code]and[.c-inline-code])
  • Bitwise OR ([.c-inline-code]or[.c-inline-code])
  • Bitwise XOR ([.c-inline-code]xor[.c-inline-code])

Bit shift operators

The shift right operator can be used to perform division by powers of 2.

  • Shift Right ([.c-inline-code]shr[.c-inline-code])
  • Shift Left ([.c-inline-code]shl[.c-inline-code])

Comparison operators

The result of comparison operations is an encrypted boolean (ebool). In the backend, the boolean is represented by an encrypted unsigned integer of bit width 8, but this is abstracted away by the Solidity library.

  • Equal ([.c-inline-code]eq[.c-inline-code])
  • Not Equal ([.c-inline-code]ne[.c-inline-code])
  • Greater Than or Equal ([.c-inline-code]ge[.c-inline-code])
  • Greater Than ([.c-inline-code]gt[.c-inline-code])
  • Less Than or Equal ([.c-inline-code]le[.c-inline-code])
  • Less Than ([.c-inline-code]lt[.c-inline-code])

Additional operators

  • Minimum ([.c-inline-code]min[.c-inline-code])
  • Maximum ([.c-inline-code]max[.c-inline-code])
  • Negation ([.c-inline-code]neg[.c-inline-code])
  • Logical NOT ([.c-inline-code]not[.c-inline-code])

Now that we reviewed the common operators, let's deep dive into FHE specific ones.

CMUX operator

This operator offers the ability to assign a value among two possibilities based on a condition, while preserving the encrypted state of the data.

This operator takes three inputs. The first input [.c-inline-code]b[.c-inline-code] is of type [.c-inline-code]ebool[.c-inline-code] and the two others of type [.c-inline-code]euintX[.c-inline-code]. If [.c-inline-code]b[.c-inline-code] is an encryption of [.c-inline-code]true[.c-inline-code], the first integer parameter is returned. Otherwise, the second integer parameter is returned.

// if (b == true) return val1 else return val2
function cmux(ebool b, euint8 val1, euint8 val2) internal view returns (euint8) {
  return TFHE.cmux(b, val1, val2);
}

Generating random encrypted integers

Random encrypted integers can be generated fully on-chain.

WARNING: Don't use in production! Integers are currently generated in the plain via a PRNG whose seed and state are public, while the state is on-chain. An FHE-based PRNG is coming soon, where the seed and state will be encrypted.

// Generate a random encrypted unsigned integer `r`.
euint32 r = TFHE.randEuint32();

Decrypt

Decryption currently occurs in the validator, which has full access to the private key (note that a threshold decryption protocol is under development). This function can potentially lead to information leakage, therefore, you should consider using [.c-inline-code]cmux[.c-inline-code] instead for safer operations.

function bid(bytes calldata encryptedBid) internal {
  euint32 bid = TFHE.asEuint32(encryptedBid);
  ebool isAbove = TFHE.le(bid, highestBid);

  // Be sure that the bid is above current highestBid
  require(TFHE.decrypt(isAbove));

  // Replace highest bid
  highestBid = bid;
}

Reencrypt

Remember that all inputs are encrypted under the network public key. To return a user specific data, we need to reencrypt the given data under the user-provided public key. 

// returns the decryption of `ciphertext`, encrypted under `publicKey`.
function reencrypt(euint32 ciphertext, bytes32 publicKey) internal view returns (bytes memory reencrypted)

Optimize performance using optReq

Instead of interrupting the transaction flow by decrypting conditions at each occurrence, we accumulate these conditions until the end of the transaction and perform a single decryption with [.c-inline-code]optReq[.c-inline-code]. If all conditions are satisfied, state changes are applied. If not, they are reverted. This approach streamlines the process and enhances efficiency.

Note: this efficiency comes at the price of paying the full transaction gas cost if one of the boolean conditions is false.

Finally, you now have the ability to use our specialized Remix instance, designed specifically to handle input encryption, but you can also take advantage of our dedicated Hardhat template, which includes a single-line command for launching a local fhEVM instance.

Additional links

Read more related posts