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.
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.
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:
Block execution in fhEVM-native is split into two parts:
Symbolic Execution
FHE Computation
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. 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 Symbolic Execution.
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.
The TFHEExecutor 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.
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.