Inner workings of the RPC proxy

After releasing Kevlar, we received several questions regarding the inner workings of the RPC proxy. Kevlar is made up of two main components: the sync client, and the RPC proxy. The RPC Proxy is implemented as a separate library, “Patronum” (github.com/shresthagrawal/patronum).

Patronum takes as input a trusted blockHash from the sync client and some untrusted RPC endpoints. It exposes RPC endpoints such that every RPC response is verified against the trusted blockHash. Let's look into how Patronum verifies every RPC call using just a blockHash.

For simplicity, let's assume that all RPC calls are made for the current blockNumber. Regarding an RPC call about the block itself (eth_getBlockByNumber/Hash), we can fetch the block from the untrusted RPC and verify that the hash of the block matches the known trusted blockHash.

Once we have verified a block we have expanded our knowledge to everything in that block: stateRoot, transactionTrieRoot, transactionReciptRoot, previousBlockHash, etc. We will use this to verify other RPC calls.

For calls such as eth_getBalance, getCode, getTransactionCount, and getStorageAt, we can call the eth_getProof on the untrusted RPC to get the Merkle inclusion proof and verify it against the known stateRoot. Implementing eth_call and eth_estimateGas is the tricky part.

A simple solution is to simulate the call on a local EVM with a custom stateManager. Every time the stateManager is called for the nonce, balance, code, or contract storage, we call eth_getProof on the untrusted RPC and perform the Merkle inclusion proof to the known stateRoot.

This will be extremely slow for RPC calls accessing multiple states. Instead of loading the state one by one, per request, we load everything at the start and then simulate the call. This allows us to batch eth_getProof calls and reduce the number of necessary RPC interactions.

We call eth_createAccessList on the untrusted RPC to generate a list of the accounts and contract slots that will be accessed. Then, we batch-call eth_getProof for every item in the list, verify the proofs, load it into the stateManager, and simulate the call on the local EVM.

For eth_getTransactionRecipt, we fetch the block with all the transactions and verify the transactionTrieRoot. For calls regarding past blocks, we recursively roll back by verifying the previous block using the previousBlockHash present in the current block, and so on.

We were able to build a trustless RPC server on top of an untrusted RPC using just the blockHash that we obtained from the sync client. We do not need any custom RPC endpoints. Minor details have been left out for simplicity.

The exact implementation is available at github.com/shresthagrawal/patronum/blob/main/src/provider.ts. To complete the picture, we need to obtain the “trusted blockHash” in a trustless manner. For that, there are numerous methods, outlined in our paper arxiv.org/pdf/2209.08673.pdf.