Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The fhEVM employs symbolic execution - essentially, inputs to FHE operations are symbolic values (also called handles) that refer to ciphertexts. We check constraints on these handles, but ignore their actual values.
Inside the Executor (in fhEVM-native) and inside the Coprocessor, we actually execute the FHE operations on the ciphertexts the handles refer to. If a new ciphertext is generated as a result of an FHE operation, it is inserted into the blockchain for fhEVM-native (into the ciphertext storage contract, see Storage) or into the DB and DA for Coprocessor under a handle that is deterministically generated by the TFHEExecutor contract.
Note: All those contracts are initially deployed behind UUPS proxies, so could be upgraded by their owner at any time. Unless if the owner renounces ownership, after which the protocol could be considered imumutable.
Symbolic execution on the blockchain is implemented via the TFHEExecutor contract. One of its main responsibilites is to deterministically generate ciphertext handles. For this, we hash the FHE operation requested and the inputs to produce the result handle H:
Inputs can either be other handles or plaintext values.
The ACL contract enforces access control for ciphertexts. The model we adopt is very simple - a ciphertext is either allowed for an address or not. An address can be any address - either an EOA address or a contract address. Essentially, it is a mapping from handle to a set of addresses that are allowed to use the handle.
Access control applies to transfering ciphertexts from one contract to another, for FHE computation on ciphertexts, for decryption and for reencryption of a ciphertext to a user-provided key.
Data in the ACL contract grows indefinitely as new ciphertexts are produced. We might want to expose ways for developers to reclaim space by marking that certain ciphertexts are no longer needed and, consequently, zeroing the slot in the ACL. A future effort will look into that.
The KMSVerifier contract allows any dApp to verify a received decryption. This contract exposes a function verifyDecryptionEIP712KMSSignatures
which receives the decryption result and signatures coming from the TKMS.
KMS signers addresses are stored and updated in the contract.
The KMSVerifier contract is also responsible for checking the signatures of KMS signers when a user is inputing a new ciphertext, since this process involves the user firstly sending a ZKPoK to be verified by the KMS nodes. If the proof verifies successfully at the KMS, each KMS signer will sign a hash of the new user input and the signatures will be returned to the user, who will then be able to input new handles onchain. This is done via the verifyInputEIP712KMSSignatures
function.
The InputVerifier checks the coprocessors accounts' signatures which include the computed handles (the KMS signatures only include the hash of the packed ciphertext, not the handles). We trust the handles computation done by the coprocessors before using them in transactions onchain.
This contract is needed for security, especially for coprocessor where we could not tweak native gas to be used for FHE operations as for native: FHE gas price constants.
This contract tracks the FHEGas consumed in each block, and reverts the transactions inside a block if the FHEGas block limit is exceeded.
The DecryptionOracle is an onchain contract designed to interact with an offchain Gateway component that handles decryption requests. When a dApp calls the requestDecryption
function, the DecryptionOracle
contract emits an event that is caught by the Gateway service.
Note: It is possible to have multiple Gateways, so multiple Gateway contracts can also be deployed. This is the only contract from this documentation page that is not strictly part of "core fhEVM" contracts, and as such, it should not be considered as a "trusted" contract. We only trust the KMS and the core fhEVM contracts. The Gateway is only bridging trust from host chain to KMS chain via storage proofs, and from KMS chain to the host chain via the signatures from KMS signers.
The following diagram shows an fhEVM-native blockchain with 4 validators.
Note: For brevity, we don't show P2P connections between validators and the full node in the diagram.
Each validator has two components:
the validator node software that executes blocks and connects to other validators over the blockchain's P2P network
the Executor that is responsible for the actual FHE computation
The Executor exposes an API that the validator node uses to send FHE computation requests.
A full node is similar to validators in the sense that it executes all blocks. The difference is that the full node doesn't have stake in the network and, therefore, cannot propose blocks. The full node has all the blockchain data locally. It can be used by the Gateway over RPC or WebSocket endpoints, allowing the Gateway to fetch storage proofs, fetch ciphertexts, listen for events on the fhEVM blockchain, etc.
The Gateway is a client from the TKMS' perspective and sends decryption/reencryption transactions, listens for "decryption ready" events, etc.
A dApp uses the fhevmjs library to interact with the fhEVM. Some examples are:
connect over HTTP to the Gateway for reencryptions
encrypt and decrypt data from the blockchain
send transactions via a full node
get the FHE public key from a full node
The TKMS is used to manage secret FHE key material and securely execute decryptions, reencryptions, key generation, etc. The TKMS is itself a blockchain. See TKMS.
Block execution in fhEVM-native is split into two parts:
Symbolic Execution
FHE Computation
At the end of the block, the EVM sends a networking call to the Executor with the accumulated FHE computations. The Executor is free to do the FHE computations via any method, e.g. in parallel, on a cluster of compute nodes, via CPUs, GPUs, FPGAs or ASICs. The EVM waits until FHE computation for the block is done.
Finally, when results are returned to the EVM, it persists onchain the ciphertexts whose handles have been SSTOREd during symbolic execution. That way the EVM can avoid persisting ciphertexts that are intermediate results and are never actually stored by the smart contract developer.
Since the Executor can extract data dependencies from the SyncCompute
request, it can use them to execute FHE computations in parallel.
Different scheduling policies can be set for FHE computation via the FHEVM_DF_SCHEDULE
environment variable with possible choices: LOOP, FINE_GRAIN, MAX_PARALLELISM, MAX_LOCALITY.
Symbolic execution is a method of constructing a computational graph of FHE operations without actually doing the FHE computation. It works by utilizing what we call a ciphertext handle. The handle could be thought of as an unique "pointer" to a given FHE ciphertext.and is implemented as a 32-byte value that is a result of applying a hash function to either an FHE ciphertext or other handles. Symbolic execution also checks constraints on input handles (e.g. the access control list, whether types match, etc.).
Inputs can either be other handles or plaintext values.
Note that fhEVM-native and fhEVM-coprocessor send both input handles and result handles for FHE computation. It is able to do that, because result handles are computed symbolically in the TFHEExecutor contract. That allows for parallel FHE computation by analysing which computations are independent.
The Executor or Coprocessor can detect a conflict if an output of computation A (or the output of another computation depending on the output of A) is also used as an input in a subsequent computation B. We call these computations dependent
and we need to execute them in order.
On the other hand, if two computations have inputs that are not related to their outputs, we call them independent
and can schedule them to run in parallel.
When we talk about inputs, we refer to encrypted data users send to an fhEVM-native blockchain or an fhEVM-coprocessor. Data is in the form of FHE ciphertexts. An example would be the amount to be transferred when calling an ERC20 transfer function.
It is important that confidential data sent by users cannot be seen by anyone. Without measures, there are multiple ways that could happen, for example:
anyone decrypting the ciphertext
anyone doing arbitrary computations via the ciphertext (e.g. adding 0 to it), producing a new ciphertext that itself is decrypted (including malicious actors using ciphertexts of other users)
using the ciphertext in a malicious contract that leads to decryption
Furthermore, if users are allowed to send arbitrary ciphertexts (including malformed ones or maliciously-crafted ones), that could lead to revealing data about the FHE secret key.
Therefore, we employ zero-knowledge proofs of knowledge (ZKPoK) of input FHE ciphertexts that guarantee:
ciphertext is well-formed (i.e. encryption has been done correctly)
the user knows the plaintext value
the input ciphertext can only be used in a particular smart contract
The ZKPoK is verified by the KMS which delivers a signature (KMS_S) to the user. When the input byte array is passed to an TFHE.asEuintXX()
function to convert from a ciphertext to a handle that can be used in smart contracts for FHE operations, the KMS_S is verified.
To greatly reduce the size of FHE ciphertexts inputs, we utilize a feature called compact lists. It allows us to pack multiple values efficiently. It is useful when there is only one input and even more so when the are multiple inputs in a call to a smart contract.
We define the einput
type that refers to a particular ciphertext in the list. The list itself is serialized and passed as a byte array. For example, inputA
and inputB
refer to ciphertexts in the list and the serialized list is inputProof
:
Note that inputProof
also contains the ZKPoK.
Handling inputs requires a few steps. The first one is for the user to retrieve public key material from the Gateway. The second is to encrypt plaintext inputs and compute the associated ZKPoK. Last step is to use inputs as "usual" inputs in a smart contract.
The first step to generate an encrypted input is to retrieve the blockchain related FHE public key material. The Gateway is the component the user queries to get that material.
The Gateway is exposing a /keys
endpoint that returns the FHE public key and CRS alongside a signature. Users are able to verify them using KMSVerifier smart contract.
In this phase, the user encrypts the plaintext input with the FHE public key to get ciphertext C
and compute the proof ZkPoK
. C
is bounded to be used with a contractAddress
and by a callerAddress
. The goal is for C
to be signed the KMS to enable the usage of the input within smart contracts later.
C == ciphertext - Encrypted with the blockchain FHE public key
ZKPoK == Zero-Knowledge Proof of Knowledvge - Computed on the user side
eInput == type + index
S == Signature
When the user receives the KMS signature, it means that the ZKPoK has been verified by the KMS and the input could be used within fhEVM. This is quite useful because on the fhEVM only the KMS signature will be verified and that is faster than verifying a ZkPoK.
Ciphertexts in fhEVM-native are stored onchain in the storage of a predefined contract that has no code and is used just for ciphertexts. At the time of writing, its address is 0x5e.
Contract storage in the EVM is a key-value store. For ciphertexts, we use the handle as a key and the value is the actual ciphertext.
Furthermore, stored ciphertexts are immutable, making ciphertext storage append-only.
Ciphertexts can be read by anyone. We expose the GetCiphertext
function on the FheLib
precompiled contract. Nodes/validators must support it.
The GetCiphertext
function returns a serialized TFHE ciphertext given:
the ebool/e(u)int value (also called a handle) for which the ciphertext is requested
GetCiphertext only works via the eth_call
RPC.
To call GetCiphertext via eth_call
, the following Python can serve as an example:
Symbolic execution happens onchain, inside the contract (inside the EVM). Essentially, the EVM accumulates all requested FHE operations in a block with their input handles and the corresponding result handles. It also remembers which result handles are stored via the SSTORE opcode. No FHE computations are done inside the EVM itself.
For more on symbolic execution, please see .
The contract is deployed when the chain is created and is at a well-known address that is also known by blockchain nodes. When a node (validator or full node) detects a call to this address (a CALL or STATICCALL opcode), the EVM running in the node looks at the function signature and determines which FHE computation is being requested. The result handle is the result of this particular call to the TFHEExecutor contract and the EVM can accumulate it in the computations list for the block.
Symbolic execution onchain is implemented via the contract. One of its main responsibilites is to deterministically generate ciphertext handles. For this, we hash the FHE operation requested and the inputs to produce the result handle H:
For an fhEVM-native blockchain to operate and execute FHE computations, certain contracts need to be available when creating the chain - see Contracts. Strictly speaking, these contracts don't have to be available in the genesis block and can be deployed in the second block of the chain, at runtime.
FHE-related keys need to available for the chain to operate properly. For example, a public FHE execution key is needed at the Executor to be able to compute on encrypted data.
As a convenience, the FHE public key can also be stored on validators/full nodes.
The following diagram shows an fhEVM-coprocessor that is integrated alongside an existing host blockchain.
An important note to point out is that the Coprocessor is an offchain component. It contains the following sub-components:
host blockchain full node that executes all blocks on the host blockchain
an executor that does FHE computation
a local database for storing FHE ciphertexts
Essentially, as the Coprocessor executes blocks and when an FHE operation is detected, the executor sub-component would actually execute the FHE computation and load/store FHE ciphertexts from the local database (and the DA). For more on execution, please look at Symbolic Execution and FHE Computation.
The Data Availability (DA) is a publicly-verifiable database that is a mirror of the local Coprocessor database. The reason for having is to allow anyone to verify the behaviour of the Coprocessor by examining the results it posts to it.
The Gateway is responsible for handling input verification, decryption and reencryption and host blockchain validator set updates, all via/in the KMS.
Block execution in fhEVM-coprocessor is split into two parts:
Symbolic Execution (onchain)
FHE Computation (offchain)
Symbolic execution happens onchain, inside the TFHEExecutor contract (inside the EVM). Essentially, the EVM accumulates all requested FHE operations in a block with their input handles and the corresponding result handles. Then, at the end of block execution, it sends an AsyncCompute
request to the coprocessor such that FHE computation can be done eventually. Note that FHE computation can be done at a future point in time, after the block has been committed on the host blockchain. We can do that, symbolic execution only needs handles and doesn't actual FHE ciphertexts. Actual FHE ciphertexts are needed only on decryption and reencryption, i.e. when a user wants to see the plaintext value.
For more on symbolic execution, please see Symbolic Execution.
Note that, for now, we omit the Data Availability (DA) layer. It is still work in progress and the Coprocessor only inserts FHE ciphertexts into its local DB. Eventually, we would like that FHE ciphertexts are also inserted into the DA.
Since the Coprocessor can extract data dependencies from the AsyncCompute
request, it can use them to execute FHE computations in parallel.
At the time of writing, the Coprocessor uses a simple policy to schedule FHE computation on multiple threads. More optimal policies will be introduced in the future and made configurable.